跳过主内容

"Stripe Payments in Capacitor:",""

Learn how to implement Stripe Payment Links in your Capacitor app to process digital goods payments in compliance with new Apple guidelines effective May 1, 2025.

"",""

"",""

"",""

Stripe Payments in Capacitor: New Apple Guidelines

Implementing Stripe Payment Links in Capacitor Apps Following New Apple Guidelines

"","" "","""",""

The Epic Battle That Changed Mobile Payments Forever

这场改变移动支付历史的战争的道路漫长而充满争议。它始于2020年8月,当时Epic Games,Fortnite的创造者,故意违反了苹果App Store的指南,通过在Fortnite中实施直接支付选项来绕过苹果的30%佣金。苹果迅速从App Store中移除了Fortnite,Epic随后提起了诉讼,挑战苹果对iOS应用程序分发和内购的控制。

经过多年的法律斗争、上诉和反诉,法院终于裁定苹果必须允许开发者在应用程序外直接指向支付方法。这一决定彻底改变了App Store生态系统的经济模型,这个模型自2008年成立以来一直沿用不变。

最终裁决 - 无法上诉

这一裁决尤其重要,因为它是最终的,无法再上诉。最高法院在2025年初拒绝了苹果的上诉,确认了下级法院的决定是法律的。这意味着开发者可以自信地实施外部支付方法,苹果无法通过进一步的法律挑战来逆转这一决定。

法律保证平等的处理

最重要的是,裁决明确指出苹果不能对使用外部支付方法的应用程序进行歧视。法院明确禁止苹果从事:

  1. 对使用外部支付方法的应用程序征收额外费用或施加额外要求
  2. 在搜索结果或推荐中给使用苹果IAP系统的应用程序优先处理
  3. 使用技术措施使外部支付体验不如苹果自己的系统
  4. 超出基本消费者信息的繁琐披露要求

这些明确的保护措施意味着开发者可以在不担心苹果可能的回击或歧视的情况下实施Stripe或其他外部支付提供商。平衡地板已经被法律平等化,苹果必须无论应用程序选择的支付方法都对所有应用程序进行平等处理。

这一裁决代表了对苹果围栏式园地方法的最重要挑战,也标志着移动应用程序盈利方式的转折点。长期以来,苹果30%的佣金(小企业为15%)一直是开发者抱怨的对象,这一裁决为开发者提供了更高的利润率和更大的对客户体验的控制权的途径。

使用Stripe而不是苹果内购的财务好处

这一变化的财务影响对开发者来说是显著的:

  • 降低支付处理费用:苹果通常对内购征收30%的佣金(小企业为15%),而Stripe的费用仅为2.9% + $0.30每笔交易。这一差异可以显著增加您的利润率。

  • 更快的付款: 与 Apple 合作时,您通常需要等待 45-90 天才能收到资金。另一方面,Stripe 将支付金额直接存入您的银行账户,仅需 2-3 个工作日。

  • 简化退款流程: 直接通过 Stripe 的控制台处理退款,而不是通过 Apple 的复杂退款系统。

这些成本节约和改善的现金流可以成为游戏的关键点,尤其是对于较小的开发者和企业。

在本文中,我们将探讨如何在您的 Capacitor 应用程序中实现 Stripe 支付链接,以利用这些新规则,同时确保遵守 Apple 的 更新的指南.

本实施基于 Stripe 支付链接的官方文档,并针对 Capacitor 应用程序进行了特别的适配。

了解新指南

App Store Review 指南的更新版本现在允许开发者将用户指向外部网站进行支付处理,特别是针对数字产品和订阅。这个变化目前仅适用于在美国 App Store 分发的应用程序。

需要了解的关键点:

  1. 您现在可以在应用程序中链接到数字商品的外部付款选项
  2. 这仅适用于美国App Store中的应用
  3. 您仍然必须遵守苹果的披露要求
  4. 您仍然负责所有客户支持和退款处理

让我们深入到技术实现:

首先,在Stripe控制台中创建一个付款链接:

  1. 导航到Stripe控制台中的付款链接部分
  2. 点击“+新”创建一个新的付款链接
  3. 定义您的产品或订阅详细信息
  4. 在“付款后”设置中,选择“不显示确认页面”
  5. 设置一个通用链接作为您的成功 URL(我们稍后会配置)
  6. 点击“创建链接”以生成您的付款链接

为了将用户重定向回您的应用程序后付款完成,配置通用链接:

  1. 创建一个 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"
          }
        ]
      }
    ]
  }
}
  1. 确保它以正确的 MIME 类型服务 https://yourdomain.com/.well-known/apple-app-site-association

  2. 配置您的__CAPGO_KEEP_0__应用程序以处理通用链接,通过添加适当的特权。首先,在您的 application/json

  3. Configure your Capacitor app to handle universal links by adding the proper entitlement. First, in your 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;
  1. 打开您的 Xcode 项目
    • 选择您的应用程序目标
    • 向 Xcode 项目添加关联域名特权:
    • 前往 “签名 & 权限”
    • 点击 ”+ 权限” 并选择 “关联域名”
    • 添加 applinks:yourdomain.com

步骤 3:创建一个回退页面

创建一个回退页面来处理应用未安装的情况:

<!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 保存过付款信息的用户会自动获得凭证。这会创建一个更流畅的支付体验,用户不需要重新输入信用卡信息,显著降低了摩擦和放弃率。

配置应用来处理用户被重定向回来的 Universal 链接:

  1. 首先,安装 App 插件:
npm install @capacitor/app
  1. 在您的应用中注册 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'));

Android 兼容性

让我们明确一下:Epic v. Apple 案例已经彻底改变了移动支付的格局。它不仅直接影响 iOS 应用,还加强了 Android 开发者使用外部支付方法的立场。

Android 开发者现在可以完全信心地实施外部支付解决方案。 苹果案例所设的先例有效地保护了跨平台的开发者免受潜在的未来限制。这个法院的决定已经验证了许多 Android 开发者多年来一直在做的事情——提供低收费的替代支付选项。

Google Play 商店一直以来都比苹果更宽容对外部支付方法,现在随着法律先例的确立,几乎没有风险在您的 Android 应用中实施 Stripe 或其他外部支付提供商。您可以继续这些实现,知道您站在坚实的法律基础上。

我们为 iOS 提供的实现在 Android 设备上几乎完全相同。由于 Google Play 商店对外部支付方法没有苹果一样的限制,因此您可以使用相同的 Stripe 支付链接方法而无需特殊的披露对话框。

要处理深度链接(与 iOS universal 链接类似),您需要:

  1. 设置 App Links AndroidManifest.xml 处理重定向 URL
  2. 创建一个 .well-known/assetlinks.json 在您的域名上创建一个文件,包含您的应用的详细信息
  3. 使用相同的 appUrlOpen 处理成功支付的逻辑

The beauty of Capacitor is that once you’ve implemented the platform-specific configurations, the actual payment flow code remains the same across both platforms.

在两种平台上保持一致。

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>

以下是 Vue 中的一个支付按钮组件示例,您可以将其添加到您的

应用中:

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,或者在服务器端实现此检查以增加安全性。

注意:: While this approach works, it’s important to remember that IP geolocation isn’t always 100% accurate. For mission-critical applications, consider using multiple detection methods or allowing users to manually select their region.

More Accurate Location Detection with Capacitor Plugins

For more accurate location detection, you can use the Capacitor Geolocation plugin along with @capgo/capacitor-nativegeocoder to determine the user’s country with higher precision:

  1. 为了更准确地检测位置,您可以使用 __CAPGO_KEEP_0__ 插件以及 @__CAPGO_KEEP_1__/__CAPGO_KEEP_2__-nativegeocoder 来确定用户的国家:
npm install @capacitor/geolocation @capgo/capacitor-nativegeocoder
  1. Configure the plugins in your Capacitor project. Add the following to your 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;
  1. 在 __CAPGO_KEEP_0__ 项目中配置插件。将以下内容添加到您的
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 请记住在 AndroidManifest.xml (iOS) 和

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>

For 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;
}

确保应用商店符合性

为了确保您的实现符合苹果的指南:

  1. 包含适当的关于外部购买的披露
  2. 在 App Store 中,必须实现一个弹出窗口,告知用户他们即将离开应用程序
  3. 不要试图绕过 Apple 在应用程序内购买的佣金
  4. 清晰地告知用户,Apple 不负责交易

以下是实现所需的披露弹出窗口的示例

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);
  }
}

测试您的实现

要测试您的实现:

  1. 在您的应用程序中点击支付按钮,应该显示披露并打开 Stripe 支付页面
  2. 使用 Stripe 测试卡完成测试支付 4242 4242 4242 4242
  3. 支付后,您应该通过 universal 链接返回您的应用程序
  4. 检查您的 webhook 是否接收到 checkout.session.completed 事件

结论

The ability to use external payment options for digital goods in iOS apps is a significant change that gives developers more flexibility. While this change currently only applies to apps in the U.S. App Store, it provides an important alternative to Apple’s in-app purchase system.

By using Stripe Payment Links with Capacitor, you can quickly implement a streamlined checkout experience while maintaining compliance with Apple’s guidelines. This approach also gives you the advantage of Stripe’s robust payment infrastructure, lower processing fees (3% vs 30%), and much faster payouts (days instead of months) compared to Apple’s in-app purchase system.

Remember that you’ll need to handle all customer support and refund issues directly, as these transactions occur outside of Apple’s ecosystem.

Have you implemented Stripe Payment Links in your Capacitor app? Share your experience in the comments below!

常见问题

Q: 这种方法是否符合苹果的指南?
A: 是的,截至2025年5月1日,苹果允许将数字产品和服务的链接外部付款方法,提供您包含所需的披露。

Q: 使用外部付款方法时,我是否需要支付苹果的佣金?
A: 不,新规则的主要好处之一是,处理在苹果系统外的付款不受其佣金。

Q: 我的公司是否需要在美国注册才能利用这些新规则?
A: 不管公司在世界的哪个角落,只要你的应用程序在美国App Store可用,用户在美国的购买者就可以使用外部支付方法。判决适用于市场(美国App Store)和用户的位置,而不是公司的位置。这意味着来自欧洲、亚洲、南美洲或其他任何地方的开发者都可以为他们的美国客户实施Stripe Payment Links。

Q: 如果用户试图使用外部支付选项而位于美国以外的地区会发生什么?
A: 您应该实现区域检测(如文章中所示),只向美国用户提供外部支付选项。对于其他地区,您应该继续使用Apple的内购系统。

Q: 我可以用它来购买物理商品或在应用程序外部消费的服务吗?
A: 是的,苹果一直允许外部支付方法用于物理商品和在应用程序外部消费的服务(如出租车或外卖)。

为 Capacitor 应用提供实时更新

当 web 层面的 bug 在 live 时,通过 Capgo 发布修复,而不是等待几天的应用商店审批。用户在后台接收更新,而原生变化仍然在正常的审批路径中

立即开始

最新博客

Capgo 为您提供创建真正专业的移动应用所需的最佳见解。