PAYATHON 2026

Hybrid Mobile Application 如何集成支付系统?

支付小周

结论

如果应用通过 App Store 分发,并在应用内销售会员、虚拟币、数字内容或功能解锁,通常需要使用 Apple 的应用内购买系统(In-App Purchase,底层为 StoreKit)。不能直接在 WebView 中使用 Stripe、PayPal 或网页收银台绕过购买流程。

如果销售的是实体商品或线下服务,例如外卖、打车、酒店或实物商品,通常应该使用第三方支付,而不是 In-App Purchase。

是否采用 Hybrid 架构不影响这一判断。审核关注的是应用“卖什么”,而不是使用 Cordova、Capacitor、React Native 还是自研 WebView。

为什么原支付系统可能被拒绝

App Store 审核意见中的 integrated payment system 通常可能是指 Apple In-App Purchase,但具体仍要以审核反馈所引用的条款为准。常见的拒绝原因有:

  • 在 WebView 中销售数字内容,却通过银行卡、Stripe 或 PayPal 收款。
  • 提供外部购买按钮、二维码或跳转链接,引导用户绕过 In-App Purchase。
  • 客户端自行判定购买成功,没有确认 Apple 返回的交易结果。
  • 应用没有提供完善的“恢复购买”功能。
  • 商品尚未在 App Store Connect 中创建,或者商品状态、协议和税务信息没有配置完成。

不同地区、商品类型和应用类别可能适用不同规则。如果审核信息只写了 “integrated payment system”,可以在 App Store Connect 的 Resolution Center 中请审核人员说明对应的审核条款和商品类型。

推荐的集成方式

1. 先确定收费类型

数字商品通常包括:

  • 会员订阅
  • 去广告
  • 高级功能解锁
  • 虚拟币、道具
  • 视频、课程、电子书等数字内容

这类商品需要根据实际用途选择合适的类型:

  • Consumable
  • Non-Consumable
  • Auto-Renewable Subscription
  • Non-Renewing Subscription

实体商品和现实世界中的服务通常使用第三方支付,同时遵守支付服务商及所在地区的合规要求。

2. 在 App Store Connect 创建商品

在 App Store Connect 中为应用创建 In-App Purchase 商品,并配置以下内容:

  • Product ID
  • 商品类型
  • 价格
  • 本地化名称与描述
  • 订阅组和订阅周期(如适用)
  • 审核截图与审核说明

代码中的 Product ID 必须与 App Store Connect 完全一致。还要确认付费应用协议、银行信息和税务信息均已完成,否则商品可能无法正常测试或销售。

3. 为 Hybrid 应用增加原生支付桥接层

网页 JavaScript 无法直接、完整地调用 StoreKit。通常可以选择以下方案:

  • 使用维护状况良好的 Hybrid 框架 In-App Purchase 插件。
  • 编写 iOS 原生插件,通过 JavaScript Bridge 调用 StoreKit 2。

不要只在 WebView 中嵌入支付网页。数字商品的下单、交易监听和恢复购买都应由原生层处理。

StoreKit 2 原生示例

下面是一个简化的 Swift 示例,用于加载商品和发起购买。这里只展示核心流程。实际项目还需要处理交易监听、服务器校验、错误映射和权益同步。

import StoreKit

enum PurchaseError: Error {
    case productNotFound
    case verificationFailed
}

final class PurchaseService {
    func loadProducts(productIDs: Set<String>) async throws -> [Product] {
        try await Product.products(for: productIDs)
    }

    func purchase(productID: String) async throws -> Transaction? {
        let products = try await Product.products(for: [productID])

        guard let product = products.first else {
            throw PurchaseError.productNotFound
        }

        let result = try await product.purchase()

        switch result {
        case .success(let verification):
            let transaction = try verified(verification)

            // 将交易信息发送给业务服务器校验并发放权益。
            // 服务器确认后再结束交易。
            await transaction.finish()
            return transaction

        case .pending:
            return nil

        case .userCancelled:
            return nil

        @unknown default:
            return nil
        }
    }

    func restoreEntitlements() async throws -> [Transaction] {
        var transactions: [Transaction] = []

        for await result in Transaction.currentEntitlements {
            let transaction = try verified(result)
            transactions.append(transaction)
        }

        return transactions
    }

    private func verified<T>(
        _ result: VerificationResult<T>
    ) throws -> T {
        switch result {
        case .verified(let value):
            return value
        case .unverified:
            throw PurchaseError.verificationFailed
        }
    }
}

应用启动后还应持续监听交易更新,以免遗漏待处理的交易:

import StoreKit

func observeTransactions() -> Task<Void, Never> {
    Task.detached {
        for await result in Transaction.updates {
            guard case .verified(let transaction) = result else {
                continue
            }

            // 先提交业务服务器验证、同步用户权益,
            // 成功后再调用 finish()。
            await transaction.finish()
        }
    }
}

JavaScript Bridge 调用示例

Hybrid 页面只负责发起请求和展示状态,购买操作由原生插件完成。桥接接口可以采用 Promise 形式:

async function buyPremium() {
  try {
    const result = await window.NativePayments.purchase({
      productId: "com.example.app.premium"
    });

    if (result.status === "purchased") {
      await refreshEntitlements();
      showMessage("购买成功");
    } else if (result.status === "pending") {
      showMessage("交易正在等待确认");
    } else if (result.status === "cancelled") {
      showMessage("购买已取消");
    }
  } catch (error) {
    console.error("Purchase failed:", error);
    showMessage("暂时无法完成购买,请稍后重试");
  }
}

async function restorePurchases() {
  try {
    await window.NativePayments.restore();
    await refreshEntitlements();
    showMessage("购买记录已恢复");
  } catch (error) {
    console.error("Restore failed:", error);
    showMessage("恢复购买失败");
  }
}

window.NativePayments 只是示例接口,并非 Apple 提供的标准 API。实际接口名称取决于所用框架、插件或自研桥接实现。

服务端校验与权益管理

生产环境不能只根据客户端返回值开通会员或发放虚拟商品。完整流程通常是:

  1. 原生层完成购买并取得已验证的交易。
  2. 客户端将交易标识和必要信息发送到业务服务器。
  3. 服务器通过 Apple 提供的服务核实交易或订阅状态。
  4. 服务器执行幂等处理,避免同一交易重复发货。
  5. 服务器记录退款、撤销、续订和过期状态。
  6. 客户端从业务服务器刷新用户权益。
  7. 权益处理成功后结束交易。

订阅业务还应考虑接入 App Store Server Notifications,让服务器及时处理续订、退款、撤销和账单问题。具体接口和字段应以当前 Apple Developer 文档为准。设计新系统时,不要直接沿用旧版 Receipt Validation 示例。

测试与提审

购买流程可以在以下环境中测试:

  • Xcode StoreKit Configuration:适合测试本地流程。
  • Sandbox 测试账号:适合验证 App Store Connect 中配置的商品。
  • TestFlight:适合测试接近正式发布的完整购买流程。

提审前至少要确认:

  • 商品可以正常加载,Product ID 配置无误。
  • 购买成功、取消、失败和 pending 状态都有对应处理。
  • 非消耗型商品和订阅支持恢复购买。
  • 重装应用或更换设备后,用户可以重新同步权益。
  • 应用内没有引导数字商品用户前往网页付款。
  • Review Notes 中已经写明审核账号、测试路径和商品说明。
  • 业务服务器能够幂等处理重复通知和重复交易。

如果应用还会发布到 Google Play,需要另外接入 Google Play Billing。Apple In-App Purchase 和 Google Play Billing 是两套独立系统,不能用同一套客户端支付实现直接替代。后端可以统一管理用户权益,但必须分别验证各个平台的交易。

备注:内容仅供参考。