在Capacitor应用中按照新苹果指南实施Stripe支付链接
自2025年5月1日起,苹果已对其App Store Review指南进行了重大修改,随着Epic v. Apple案件的判决 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每笔交易。这一差异可以显著提高您的收入率。
-
更快的付款: 与苹果相比,您通常需要等待45-90天才能收到资金。Stripe则在2-3个工作日内将付款直接存入您的银行账户。
-
简化退款流程: 可以直接通过Stripe的控制台处理退款,而不是通过苹果的复杂退款系统。
这些成本节约和改善的现金流可以对小型开发者和企业产生重大影响。
在本文中,我们将探讨如何在您的Capacitor应用中实现Stripe支付链接,以利用这些新规则,同时确保遵守苹果的更新指南。 setup-stripe-payment-in-us-capacitor.
本实施基于 Stripe的官方支付链接文档,特别针对Capacitor应用。
理解新的指南
App Store Review 指南已更新,允许开发者将用户指向外部网站进行支付处理,特别是数字商品和订阅。这一变化目前仅适用于美国App Store分发的应用。
关键点:
- 您现在可以在应用中链接到数字商品的外部支付选项
- 这仅适用于美国App Store的应用
- 您仍然必须遵守Apple的披露要求
- 您仍然负责所有客户支持和退款处理
在Capacitor应用中设置Stripe支付链接
让我们深入到技术实施:
步骤 1:在 Stripe 控制台中创建付款链接
首先,在您的 Stripe 控制台中创建付款链接:
- 导航到您的 Stripe 控制台中的付款链接部分
- 点击“+ 新建”以创建一个新的付款链接
- 定义您的产品或订阅详细信息
- 在“付款后”设置中,选择“不显示确认页面”
- 设置一个通用链接作为您的成功 URL(我们稍后会配置此项)
- 点击“创建链接”以生成您的付款链接
步骤 2:在您的 Capacitor 应用中配置通用链接
为了在付款完成后将用户重定向回您的应用,请配置通用链接:
- 在您的域上创建一个文件:
apple-app-site-associationprotectedTokens
{
"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 应用程序以处理 universal 链接,通过添加适当的特权。首先,在您的
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:创建一个 fallback 页面
在重定向 URL 处创建一个 fallback 页面,以处理应用程序未安装的情况:
<!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>
步骤 4:在您的 Capacitor 应用程序中实现付款按钮
现在,添加付款按钮到您的应用程序:
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)中打开付款链接,而不是在应用程序内浏览器中打开,这是有利的,因为那些以前使用Stripe Link保存了付款信息的用户会自动拥有他们的凭证。这会创建一个更流畅的结账体验,用户不需要重新输入信用卡信息,显著降低了摩擦和放弃率。
步骤5:在应用程序中处理Universal Links
配置应用程序以处理用户被重定向回来的Universal Links:
- 首先,安装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
最后,在您的服务器上配置一个Webhook来处理成功的付款:
// 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'));
安卓兼容性
让我们明确一下:Epic v. Apple案件已经彻底改变了移动支付的格局。它不仅直接影响了iOS应用程序,还加强了使用外部支付方法的安卓开发者的地位。
Android 开发者现在可以完全信心地实现外部付款解决方案。 苹果裁决的先例有效地保护了跨平台的开发者免受潜在的未来限制。这一法院判决已经验证了许多Android开发者多年来一直做的事情——提供低收费的替代支付选项。
Google Play商店一直比苹果更少限制外部付款方法,现在随着法律先例的建立,几乎没有风险在您的Android应用中实施Stripe或其他外部付款提供商。您可以继续这些实现,知道您站在坚实的法律基础上。
我们为iOS所涵盖的实现在Android设备上几乎完全相同。由于Google Play商店对外部付款方法没有苹果相同的限制,因此您可以使用相同的Stripe付款链接方法,无需特殊的披露对话框。
要处理深度链接(与iOS上的universal links相当),您需要:
- 在您的
AndroidManifest.xml中设置App Links来处理重定向URL - 在您的域上创建一个
.well-known/assetlinks.json文件,包含您的应用的详细信息 - 使用相同的
appUrlOpen监听器逻辑来处理成功支付
Capacitor的美妙之处在于,一旦您实现了平台特定的配置,实际支付流程code在两种平台上保持一致。
创建支付 UI
Here’s an example of a payment button component in Vue that you can add to your Capacitor app:
<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 Geolocation 插件以及 @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)文件中添加必要的权限:
(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支付页面
- __CAPGO_KEEP_0__
- 使用 Stripe 测试卡完成测试支付
4242 4242 4242 4242 - 支付后,您应该通过 universal 链接被重定向回您的应用
- 检查您的 webhook 是否接收到
checkout.session.completed事件
结论
在 iOS 应用中使用外部支付选项购买数字商品是一个重大变化,给开发者带来了更多的灵活性。虽然这个变化目前只适用于美国 App Store 的应用,但它提供了一个与 Apple 的内购系统相比的重要替代方案
通过使用 Stripe Payment Links 和 Capacitor,您可以快速实现一个流畅的支付体验,同时保持与 Apple 的指南的兼容性。对于美国以外的用户,Apple 的内购系统仍然适用 @capgo/native-purchases 实现了 StoreKit 和 Google Play Billing 直接接口。这种方法还给您带来了 Stripe 强大的支付基础设施、更低的处理费率(3% vs 30%)以及更快的付款(几天而不是几个月)相比于 Apple 的内购系统
请记住,您需要直接处理所有客户支持和退款问题,因为这些交易发生在 Apple 的生态系统之外
您是否在您的 Capacitor 应用中实现了 Stripe Payment Links?在下面的评论中分享您的体验
常见问题
Q: 这种方法是否符合苹果的指南?
A: 是的,截至2025年5月1日,苹果允许在美国App Store分发的应用程序中链接到外部支付方法,提供必要的披露即可。
Q: 使用外部支付方法时,我是否需要支付苹果的佣金?
A: 不,新规则的主要好处之一是,处理在苹果系统外的支付不受其佣金的约束。
Q: 我的公司是否需要位于美国才能利用这些新规则?
A: 不,来自世界任何地方的公司都可以实施外部支付方法,只要您的应用程序在美国App Store可用,且用户位于美国。该规定适用于市场(美国App Store)和用户位置,而不是公司位置。这意味着来自欧洲、亚洲、南美或其他任何地方的开发者都可以为其美国客户实施Stripe Payment Links。
Q: 如果用户尝试使用外部支付选项而位于美国以外的地区会发生什么?
A: 您应该实现区域检测(如文章中所示),只向美国用户提供外部支付选项。对于其他地区,您应该继续使用苹果的内购系统。
Q: 我是否可以使用此方法购买物理商品或在应用程序外部消费的服务?
A: 是的,苹果一直允许外部支付方法用于物理商品和服务(如出租车或食品配送)。
继续从 Stripe Payments 在 Capacitor: 新苹果指南
如果您正在使用 Stripe Payments 在 Capacitor: 新苹果指南 以规划安全性和合规性,连接它与 加密 加密的实施细节 合规 合规的实施细节 Capgo 安全扫描器 Capgo 安全扫描器的产品工作流 Capgo 安全 为Capgo产品工作流程中的安全性,并且 Capgo信任中心 为Capgo产品工作流程中的信任中心