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。实际接口名称取决于所用框架、插件或自研桥接实现。
服务端校验与权益管理
生产环境不能只根据客户端返回值开通会员或发放虚拟商品。完整流程通常是:
- 原生层完成购买并取得已验证的交易。
- 客户端将交易标识和必要信息发送到业务服务器。
- 服务器通过 Apple 提供的服务核实交易或订阅状态。
- 服务器执行幂等处理,避免同一交易重复发货。
- 服务器记录退款、撤销、续订和过期状态。
- 客户端从业务服务器刷新用户权益。
- 权益处理成功后结束交易。
订阅业务还应考虑接入 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 是两套独立系统,不能用同一套客户端支付实现直接替代。后端可以统一管理用户权益,但必须分别验证各个平台的交易。
备注:内容仅供参考。
