在Capacitor中实现Stripe支付链接
2025年5月1日,苹果公司在Epic v. Apple案件的判决后,实施了对App Store Review Guidelines的重大变化 Epic v. Apple案.这些变化特别允许美国的应用开发者为数字产品和服务提供外部支付方法的链接,开启了苹果内购系统的替代方案
改变了移动支付的历史
这条道路的起点在2020年8月,当时Epic Games,Fortnite的创造者,故意违反了苹果App Store的指南,通过在应用程序中直接实现支付选项,绕过苹果的30%佣金。苹果迅速从App Store中移除了Fortnite,Epic随后提起了诉讼,挑战苹果对iOS应用程序分发和内购支付的控制权。
经过多年的法律纠纷、上诉和反诉,法院终于裁定苹果必须允许开发者将用户导向应用程序外的替代支付方法。这一决定彻底改变了App Store生态系统的经济模型,这个模型自2008年成立以来一直沿用不变。
最终裁决 - 无更多上诉
这一裁决尤其重要,因为它是最终的,无法再上诉。最高法院在2025年初拒绝了苹果的上诉,确认了下级法院的决定是法律的。这意味着开发者可以自信地实施外部支付方法,苹果无法通过进一步的法律挑战逆转这一决定。
法律保证平等的处理
最重要的是,裁决明确指出苹果不能歧视使用外部支付方法的应用程序。法院明确禁止苹果:
- 对使用外部支付方法的应用程序征收额外费用或施加额外要求
- 在搜索结果中给予使用苹果IAP系统的应用程序优惠待遇或特征
- 使用技术措施使外部支付体验不如苹果自己的系统
- 超出基本消费者信息的繁琐披露要求
这些明确的保护措施意味着开发者可以在不担心苹果可能的回击或歧视的情况下实施Stripe或其他外部支付提供商。平衡地场已经被法律平等化,苹果必须无论应用程序选择的支付方法都对所有应用程序一视同仁。
这一裁决代表了对苹果围栏式园地方法的最重要挑战,并标志着移动应用程序盈利方式的转折点。对于长期抱怨苹果30%佣金(小企业减少到15%)的开发者来说,这一裁决提供了更高利润率和更大控制力来管理客户体验的途径。
使用Stripe而不是苹果内购的财务好处
这一变化的财务影响对开发者来说是显著的:
-
降低支付处理费用:苹果通常在内购中收取30%的佣金(小企业15%),而Stripe的费用仅为2.9% + $0.30每笔交易。这一差异可以显著增加您的利润率。
-
更快的付款: Apple通常需要45-90天才能将您的资金付款,而Stripe则在2-3个工作日内将付款直接存入您的银行账户。
-
简化退款流程: 可以直接在Stripe的控制台中处理退款,而不需要通过Apple的复杂退款系统。
这些成本节约和改善的现金流可以带来重大变化,尤其是对于较小的开发者和企业。
In this article, we’ll explore how to implement Stripe Payment Links in your Capacitor app to take advantage of these new rules, while ensuring compliance with Apple’s This implementation is based on Stripe’s official documentation for Payment Links, adapted specifically for Capgo apps..
Understanding the New Guidelines Stripe 支付链接官方文档,特别适用于 Capacitor 应用。
__CAPGO_KEEP_0__
__CAPGO_KEEP_0__
Stripe的官方文档
- 您现在可以在应用程序中链接到数字商品的外部付款选项
- 此功能仅适用于美国App Store中的应用
- 您仍然必须遵守苹果的披露要求
- 您仍然负责所有客户支持和退款处理
在您的Capacitor应用中设置Stripe付款链接
让我们深入到技术实现:
步骤1:在Stripe控制台中创建付款链接
首先,在您的Stripe控制台中创建一个付款链接
- 导航到Stripe控制台中的付款链接部分
- 点击“+新建”以创建一个新的付款链接
- 定义您的产品或订阅详细信息
- 在“付款后”设置中,选择“不显示确认页面”
- 设置一个通用链接作为您的成功 URL(我们稍后会配置此设置)
- 点击“创建链接”以生成您的付款链接
步骤 2:在您的Capacitor应用中设置通用链接
为了将用户重定向回您的应用程序后付款完成,配置通用链接:
- 创建一个
apple-app-site-association文件在您的域上:
{
"applinks": {
"apps": [],
"details": [
{
"appIDs": ["YOURTEAMID.com.yourdomain.yourapp"],
"components": [
{
"/": "/checkout_redirect*",
"comment": "Matches any URL whose path starts with /checkout_redirect"
}
]
}
]
}
}
-
在您的域上托管此文件:
https://yourdomain.com/.well-known/apple-app-site-association -
确保它以正确的 MIME 类型服务
application/json -
配置您的Capacitor应用程序处理通用链接,通过添加适当的特权。首先,在您的
capacitor.config.ts:
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
// Your existing app configuration (appId, appName, etc.)
plugins: {
Geolocation: {
// Request precise location access on iOS
iosLocationAccuracy: 'reduced'
}
}
};
export default config;
- 在 Xcode 项目中添加关联域名特权:
- 打开您的 Xcode 项目
- 选择您的应用程序目标
- 前往“签名和能力”
- 点击“+能力”并选择“关联域名”
- 添加
applinks:yourdomain.com
步骤3:创建一个回退页面
步骤4:在您的__CAPGO_KEEP_0__应用中实现付款按钮
<!DOCTYPE html>
<html>
<head>
<title>Redirecting...</title>
<meta http-equiv="refresh" content="0;url=https://yourdomain.com/app-download">
</head>
<body>
<p>Redirecting to download page...</p>
</body>
</html>
Step 4: Implement the Payment Button in Your Capacitor App
为什么Safari很重要
import { Capacitor } from '@capacitor/core';
export async function openPaymentLink(userEmail, userId) {
// Use your actual Stripe payment link
const baseUrl = 'https://buy.stripe.com/your_payment_link';
// Add URL parameters to customize the experience
const params = new URLSearchParams({
prefilled_email: encodeURIComponent(userEmail),
client_reference_id: userId
});
const fullUrl = `${baseUrl}?${params.toString()}`;
// Simple window.open works in both web and Capacitor
// Using _blank opens in Safari on iOS which is important for users with saved Stripe Link credentials
window.open(fullUrl, '_blank');
}
:在Safari(通过在 Safari 中(通过
window.open步骤5:在应用中处理universal链接
步骤6:在应用中处理universal链接
步骤7:在应用中处理universal链接
- 首先,安装 App 插件:
npm install @capacitor/app
- 注册 App 插件到您的应用程序:
import { App } from '@capacitor/app';
// In your initialization code
App.addListener('appUrlOpen', (event) => {
// Example URL: https://yourdomain.com/checkout_redirect?session_id=cs_test_...
const url = new URL(event.url);
if (url.pathname.startsWith('/checkout_redirect')) {
// Extract any parameters you need
const params = new URLSearchParams(url.search);
const sessionId = params.get('session_id');
// Handle successful payment
if (sessionId) {
// Verify the payment on your server if needed
verifyPayment(sessionId);
// Update UI to reflect successful purchase
updatePurchaseStatus(true);
}
}
});
async function verifyPayment(sessionId) {
// Call your backend to verify the payment
// This is optional if you're relying on webhooks
}
function updatePurchaseStatus(success) {
// Update your app UI to reflect purchase status
}
第 6 步:为订单完成设置 Webhook
上下文:关于 Capgo 页面。角色:UI 标签。位置:关于 .astro 页面。消息键 `about_how_step_label` (关于如何步骤标签)。
// Using Express.js as an example
const express = require('express');
const stripe = require('stripe')('sk_test_your_stripe_secret_key');
const app = express();
// Use raw body parser for webhook signature verification
app.post('/webhook', express.raw({type: 'application/json'}), async (req, res) => {
const sig = req.headers['stripe-signature'];
const webhookSecret = 'whsec_your_webhook_secret';
let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
} catch (err) {
console.log(`Webhook Error: ${err.message}`);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle the checkout.session.completed event
if (event.type === 'checkout.session.completed') {
const session = event.data.object;
// Retrieve client_reference_id (your user ID)
const userId = session.client_reference_id;
// Grant access to the purchased content
await grantAccess(userId, session.id);
}
res.status(200).send();
});
async function grantAccess(userId, sessionId) {
// Your logic to grant access to the purchased content
// This could be updating a database, sending a notification, etc.
}
app.listen(3000, () => console.log('Webhook server running on port 3000'));
最后,配置一个 webhook 来处理成功支付:
安卓兼容性
让我们明确一下:Epic v. Apple 案例已经彻底改变了移动支付的格局。它不仅直接影响了 iOS 应用,还加强了 Android 开发者的地位,他们一直在使用外部支付方法。 Android 开发者现在可以完全信心地实施外部支付解决方案。
苹果案例的先例实际上保护了跨平台开发者免受潜在未来限制的影响。这一法庭决定已经验证了许多 Android 开发者多年来一直做的事情——提供低费率的替代支付选项。
Google Play 商店一直比苹果商店更宽容对外部支付方法,现在随着法律先例的确立,几乎没有风险在您的 Android 应用中实施 Stripe 或其他外部支付提供商。您可以放心地实施这些功能,知道您站在坚实的法律基础上。
我们为 iOS 提供的实现几乎与安卓设备相同。由于 Google Play 商店对外部支付方法没有苹果商店那么限制,因此您可以使用相同的 Stripe Payment Links 方案,无需特殊的披露对话框。
- 在您的
AndroidManifest.xml处理重定向 URL - 创建一个
.well-known/assetlinks.json在您的域名上创建一个文件,包含您的应用程序详细信息 - 使用相同的
appUrlOpen处理成功支付的逻辑
Capacitor 的美丽之处在于,一旦您实现了平台特定的配置,实际支付流程 code 在两种平台上保持一致。
创建支付 UI
以下是 Vue 中的一个支付按钮组件示例,您可以将其添加到您的 Capacitor 应用程序中:
<template>
<div class="payment-container">
<div class="pricing-card">
<h2 class="mb-4 text-xl font-bold">{{ product.name }}</h2>
<p class="mb-6 text-gray-600">{{ product.description }}</p>
<div class="mb-6 price-tag">
<span class="text-2xl font-bold">${{ product.price }}</span>
<span v-if="product.isSubscription" class="text-sm text-gray-500">/month</span>
</div>
<button
@click="handlePayment"
class="py-3 w-full font-medium text-white bg-indigo-600 rounded-lg transition-colors hover:bg-indigo-700"
>
Purchase Now
</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { Dialog } from '@capacitor/dialog';
const props = defineProps({
product: {
type: Object,
required: true
},
userEmail: {
type: String,
default: ''
},
userId: {
type: String,
required: true
}
});
const isLoading = ref(false);
async function showExternalPaymentDisclosure() {
const { value } = await Dialog.confirm({
title: 'Leaving App for Payment',
message: 'You are about to leave this app to make a payment. Apple is not responsible for the privacy or security of payments that are not made through the App Store. All payment-related issues, including refunds, must be handled by our support team.',
okButtonTitle: 'Continue',
cancelButtonTitle: 'Cancel'
});
return value;
}
async function openPaymentLink() {
// Use your actual Stripe payment link
const baseUrl = 'https://buy.stripe.com/your_payment_link';
// Add URL parameters to customize the experience
const params = new URLSearchParams({
prefilled_email: encodeURIComponent(props.userEmail),
client_reference_id: props.userId
});
const fullUrl = `${baseUrl}?${params.toString()}`;
// Simple window.open works in both web and Capacitor
// Using _blank opens in Safari on iOS which is important for users with saved Stripe Link credentials
window.open(fullUrl, '_blank');
}
async function handlePayment() {
isLoading.value = true;
try {
// Only show the disclosure on iOS
if (window.Capacitor?.getPlatform() === 'ios') {
const userConfirmed = await showExternalPaymentDisclosure();
if (!userConfirmed) return;
}
await openPaymentLink();
} catch (error) {
console.error('Payment error:', error);
await Dialog.alert({
title: 'Payment Error',
message: 'There was an error initiating the payment. Please try again.'
});
} finally {
isLoading.value = false;
}
}
</script>
处理不同地区
由于新 Apple 指南仅适用于美国 App Store 应用程序,因此您需要一个策略来检测用户地区并应用适当的支付方法。以下是使用 IP 地理位置的更可靠方法:
import { Capacitor } from '@capacitor/core';
async function determinePaymentMethod() {
// Always use Stripe for Android
if (Capacitor.getPlatform() !== 'ios') {
return 'external';
}
try {
// Use a geolocation service to determine user's country
const response = await fetch('https://ipapi.co/json/');
const locationData = await response.json();
// Check if the user is in the United States
if (locationData.country_code === 'US') {
return 'external'; // Can use Stripe Payment Links
} else {
return 'iap'; // Must use In-App Purchases
}
} catch (error) {
console.error('Error detecting region:', error);
return 'iap'; // Default to IAP to be safe
}
}
export async function processPayment(product, userEmail, userId) {
const paymentMethod = await determinePaymentMethod();
if (paymentMethod === 'external') {
// Use Stripe Payment Links
await initiateExternalPayment(userEmail, userId);
} else {
// Use Apple's In-App Purchase
await initiateInAppPurchase(product.appleProductId);
}
}
此方法使用免费 ipapi.co 基于用户 IP 地址来确定用户的国家。您也可以使用其他地理位置服务,如 MaxMind,或者在服务器端实现此检查以增加安全性。
注意::虽然这种方法有效,但请记住 IP 地理位置检测并不总是 100% 准确。对于 mission-critical 应用程序,考虑使用多种检测方法或允许用户手动选择区域。
使用 Capacitor 插件进行更准确的位置检测
为了更准确地检测位置,您可以使用 Capacitor 地理位置插件,结合 @capgo/capacitor-nativegeocoder 来确定用户的国家,精度更高:
- 首先,安装所需的插件:
npm install @capacitor/geolocation @capgo/capacitor-nativegeocoder
- 在您的 Capacitor 项目中配置插件。将以下内容添加到您的
capacitor.config.ts:
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
// Your existing app configuration (appId, appName, etc.)
plugins: {
Geolocation: {
// Request precise location access on iOS
iosLocationAccuracy: 'reduced'
}
}
};
export default config;
- 实现基于位置的区域检测:
import { Capacitor } from '@capacitor/core';
import { Geolocation } from '@capacitor/geolocation';
import { NativeGeocoder } from '@capgo/capacitor-nativegeocoder';
async function isUserInUSA() {
try {
// Request permission first
const permissionStatus = await Geolocation.requestPermissions();
if (permissionStatus.location === 'granted') {
// Get current position
const position = await Geolocation.getCurrentPosition({
timeout: 10000,
enableHighAccuracy: false
});
// Use NativeGeocoder to reverse geocode the coordinates
const results = await NativeGeocoder.reverseGeocode({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
useLocale: true,
maxResults: 1
});
if (results.addresses.length > 0) {
// Check if the user is in the USA
return results.addresses[0].countryCode === 'US';
}
}
// If we couldn't determine location or permission denied, fall back to IP detection
return await isUserInUSAByIP();
} catch (error) {
console.error('Error detecting location:', error);
// Fall back to IP detection on error
return await isUserInUSAByIP();
}
}
async function isUserInUSAByIP() {
try {
const response = await fetch('https://ipapi.co/json/');
const data = await response.json();
return data.country_code === 'US';
} catch (error) {
console.error('Error detecting IP location:', error);
return false; // Default to false to be safe
}
}
export async function determinePaymentMethod() {
// Always use Stripe for Android
if (Capacitor.getPlatform() !== 'ios') {
return 'external';
}
// Check if user is in the USA
const isUSA = await isUserInUSA();
return isUSA ? 'external' : 'iap';
}
export async function processPayment(product, userEmail, userId) {
const paymentMethod = await determinePaymentMethod();
if (paymentMethod === 'external') {
// Use Stripe Payment Links
await initiateExternalPayment(userEmail, userId);
} else {
// Use Apple's In-App Purchase
await initiateInAppPurchase(product.appleProductId);
}
}
此实现提供了更准确的方法来确定用户是否在物理上位于美国。它首先尝试使用设备的 GPS 和本机地理编码器来确定国家。如果失败(由于权限问题或其他错误),则回退到基于 IP 的检测。
请记住在您的 info.plist (iOS) 和 AndroidManifest.xml (Android) 文件中添加必要的权限:
For iOS (ios/App/App/Info.plist):
<key>NSLocationWhenInUseUsageDescription</key>
<string>We need your location to determine which payment method to use based on regional availability.</string>
在 Android(android/app/src/main/AndroidManifest.xml):
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
使用这种方法可以准确地确定用户是否符合苹果新指南下的外部付款选项的资格。
订阅管理
使用Stripe进行付款的关键优势是可以提供和管理订阅。以下是如何在您的Capacitor应用中处理订阅管理:
1. 创建订阅管理页面
在您的应用中添加一个订阅管理页面,以显示用户的活跃订阅:
<template>
<div class="subscription-manager">
<div v-if="isLoading" class="loading-indicator">
Loading subscription data...
</div>
<div v-else-if="subscription" class="subscription-info">
<h2 class="mb-4 text-xl font-bold">Your Subscription</h2>
<div class="mb-6 plan-details">
<p><span class="font-medium">Plan:</span> {{ subscription.planName }}</p>
<p><span class="font-medium">Status:</span> {{ subscription.status }}</p>
<p><span class="font-medium">Renews:</span> {{ formatDate(subscription.currentPeriodEnd) }}</p>
</div>
<button
@click="manageSubscription"
class="py-3 w-full font-medium text-white bg-indigo-600 rounded-lg transition-colors hover:bg-indigo-700"
>
Manage Subscription
</button>
</div>
<div v-else class="no-subscription">
<p class="mb-4">You don't have an active subscription.</p>
<button
@click="goToPricingPage"
class="py-3 w-full font-medium text-white bg-indigo-600 rounded-lg transition-colors hover:bg-indigo-700"
>
View Plans
</button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { getUserSubscription } from '../services/subscription';
const subscription = ref(null);
const isLoading = ref(true);
onMounted(async () => {
try {
const userData = await getUserSubscription();
subscription.value = userData.subscription;
} catch (error) {
console.error('Failed to load subscription:', error);
} finally {
isLoading.value = false;
}
});
function formatDate(timestamp) {
return new Date(timestamp * 1000).toLocaleDateString();
}
function manageSubscription() {
// Open Stripe Customer Portal
window.open(subscription.value.portalUrl, '_blank');
}
function goToPricingPage() {
// Navigate to pricing page
// router.push('/pricing');
}
</script>
2. 订阅管理客户端
Stripe提供了一个客户端端口,允许用户管理他们的订阅。您可以从您的服务器创建一个链接到这个端口:
// Server-side code (Node.js)
const stripe = require('stripe')('sk_your_stripe_secret_key');
async function createPortalSession(customerId) {
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: 'https://yourdomain.com/account',
});
return session.url;
}
确保应用商店合规
为了确保您的实现符合苹果的指南:
- 包含适当的关于外部购买的披露
- 离开应用时请注意:您即将离开应用,是否继续?
- 避免在应用程序内进行购买时绕过苹果的佣金
- 清楚地告知用户苹果不负责交易
以下是实现所需的披露模态窗口的示例:
import { Dialog } from '@capacitor/dialog';
async function showExternalPaymentDisclosure() {
const { value } = await Dialog.confirm({
title: 'Leaving App for Payment',
message: 'You are about to leave this app to make a payment. Apple is not responsible for the privacy or security of payments that are not made through the App Store. All payment-related issues, including refunds, must be handled by our support team.',
okButtonTitle: 'Continue',
cancelButtonTitle: 'Cancel'
});
return value;
}
export async function initiateExternalPayment(userEmail, userId) {
const userConfirmed = await showExternalPaymentDisclosure();
if (userConfirmed) {
await openPaymentLink(userEmail, userId);
}
}
测试您的实现
要测试您的实现,请执行以下步骤:
- 点击您的应用程序中的付款按钮,应显示披露并打开Stripe付款页面
- 使用Stripe测试卡完成测试付款
4242 4242 4242 4242 - 付款后,您应通过universal link返回您的应用程序
- 检查您的webhook接收到
checkout.session.completed事件
结论
在 iOS 应用程序中使用外部付款选项的能力是开发者更具灵活性的一个重大变化。虽然此变化目前仅适用于美国 App Store中的应用程序,但它为开发者提供了一个重要的替代方案,避免了苹果的内购系统。
通过使用 Stripe Payment Links 和 Capacitor,您可以快速实现一个流畅的支付体验,同时保持与苹果的指南的兼容性。对于美国以外的用户,苹果的内购系统仍然适用。 在 capgo/native-purchases 中,实现了 StoreKit 和 Google Play Billing 直接接口。这一方法还为您提供了 Stripe 强大的支付基础设施、更低的处理费率(3% vs 30%)以及更快的付款(天而非月)等优势,相比于苹果的内购系统。 请记住,您需要直接处理所有客户支持和退款问题,因为这些交易发生在苹果的生态系统之外。
您是否在 __CAPGO_KEEP_0__ 应用程序中实现了 Stripe Payment Links?请在下面的评论中分享您的经验!
Have you implemented Stripe Payment Links in your Capacitor app? Share your experience in the comments below!
FAQs
A:是的,从 2025 年 5 月 1 日起,苹果允许将外部付款方法链接到应用程序中,提供了所需的披露信息。
Q:使用外部付款方法时是否需要支付苹果的佣金?
A: 使用外部支付方法时,是否需要支付苹果的佣金?
A: 不, 新规则的一个主要好处是,通过苹果系统以外的支付处理不受苹果佣金的约束。
Q: 我的公司需要位于美国才能利用这些新规则吗?
A: 您应该实现区域检测(如文章中所示),只向美国用户提供外部支付选项。对于其他地区,您应该继续使用苹果的内购系统。
A: 是的,苹果一直允许外部支付方法用于在应用程序外部消费的物理商品和服务(如出租车或食品配送)。
A: 不要停止,继续阅读Stripe Payments in __CAPGO_KEEP_0__: New Apple Guidelines。
A: 如果您正在使用Stripe Payments in __CAPGO_KEEP_0__: New Apple Guidelines
A: 来规划安全性和合规性,连接它到
继续从 Stripe Payments 在 Capacitor: 新 Apple 指南
A: 不需要,任何公司,无论其所在地在哪里,都可以实现外部支付方法,只要您的应用程序在美国App Store可用,且用户位于美国,且用户正在美国App Store中进行购买。该规定适用于市场(美国App Store)和用户的位置,而不是公司的位置。这意味着来自欧洲、亚洲、南美洲或其他任何地方的开发者都可以为其美国客户实施Stripe支付链接。 Stripe Payments in Capacitor: 符合新苹果指南 A: 是的,苹果一直允许外部支付方法用于在应用程序外部消费的物理商品和服务(如出租车或食品配送)。 Encryption 为加密的实现细节 合规 为合规的实现细节 Capgo 安全扫描器 为Capgo 安全扫描器的产品工作流程 Capgo 安全 为Capgo 安全的产品工作流程 为Capgo 信任中心的产品工作流程 为产品工作流程在Capgo信任中心。