背景
Apple 在 WWDC 2021 (iOS 15) 上开始推出了 StoreKit 2,经历过一年多的接口变更及 Bug 修复,在 iOS 16 上 StoreKit 2 已经非常稳定。
考虑到 StoreKit 2 相对 StoreKit 1 从实际支付链路到代码设计都更加健壮,在 iOS 16 发布后,我们决定在流利说的应用上更新支持 StoreKit 2。
本系列共两篇,分为 iOS 篇和后端服务篇。本文适用于 iOS 开发者,大致阅读时长 5 - 6 分钟。
我们在 StoreKit 1 中遇到了哪些问题 ?
在流利说,我们有 Pay 相关的中台服务,用于支持不同业务应用的订单,支付,摊销等支付类服务。
而在实践 StoreKit 1 中,大家遇到的最大问题是: 由于 StoreKit 1 中提供的 applicationUsername 存在跨设备等情况下丢失的问题,苹果的订单和流利说 Pay 系统中的订单无法很好的进行关联,在多流利说账号下需要一些非常 tricky 的策略/人工判定订单。
除了这个比较大的问题之外,StoreKit 1 在设计上还存在如下问题:
-
StoreKit 1 的设计相较于其他 Apple SDK 过于复杂,在 StoreKit 1 的设计上,我们需要了解 products, transactions, payments, requests, receipts, refreshes 等等,为了更好的支持支付场景,大多数时候我们必须拥有对应的 Server 端。由于其复杂性,市场上出现了相关的 SaaS 产品,用于更便捷地在项目中集成 Apple In-App Purchase。
-
退款支持的完整性,在 StoreKit 1 的场景下,用户只能找苹果进行退款,退款确认完成后 Apple 会向我们的服务发送通知,且这个链路无法进行测试。
-
无法主动地去苹果服务器获取交易历史记录,退款信息等。无法根据用户提供的苹果收据里的 orderID 主动关联上我们当前已知的订单。
-
在 StoreKit 1 中我们需要自己使用古老的 C-based
OpenSSL 库
对账单数据进行解析及验证,以防止用户通过 Hack 的方式免费使用 App 中的付费服务。 -
同一个 Apple 账号多设备交易数据方便的共享问题。
-
... ...
随着 In-App Purchase 占比 Apple 收入份额越来越高及移动互联网越来越复杂的交易场景, StoreKit 1 本身已经较难很好的支持,Apple 在时隔 10 几年后终于在 2021 年发布了 StoreKit 2。
如果你的应用只需要支持 iOS 15 及以上,建议直接使用 StoreKit 2。如果你的应用和流利说一样,需要支持较老的 iOS 版本,同样建议你在原有的基础上同时兼容 StoreKit 2 (服务端和客户端),以满足更多的支付场景。
如何支持 StoreKit 2 ?
在流利说的业务中,我们大多数时候使用的 In-App Purchase 商品类型是: Non-renewing subscriptions 。一次完整的 StoreKit 1 正常购买链路大致如下:
-
iOS 端使用 StoreKit 获取 In-App Purchase Product Id 对应的商品状态及详情
-
iOS 端使用 Product Id 向流利说 Pay 服务发起创建订单的请求,获取该次交易的订单号
-
iOS 端使用 StoreKit 向用户发起该笔商品的支付请求,并同时设置 payment 的 applicationUsername 为订单号等相关信息
-
用户完成支付,iOS 端获取支付对应的 receipts 上报后端进行验证,并带上交易的上下文信息(applicationUsername 等)
-
后端对 receipts 进行验证,验证通过后给用户发货并回调 iOS 端
-
iOS 端获得 receipts 验证结果,该笔交易完成
以下结合上述整个链路,通过 StoreKit 1 的视角带大家了解 StoreKit 2 如何进行适配及改造:
获取商品详情
在 StoreKit 2 中,获取商品详情的 API 大大简化,支持了 Swift Async/Await,一行代码就能搞定。
# StoreKit 1
let productsRequest = SKProductsRequest(productIdentifiers: identifiers)
productsRequest.delegate = self
productsRequest.start()
self.productsRequest = productsRequest
extension StoreKitService: SKProductsRequestDelegate {
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
// 获取商品状态及信息
}
}
# StoreKit 2
let storeProducts = try await Product.request(with: identifiers)
创建订单及发起支付
在 StoreKit 2 的场景下,iOS 端向流利说 Pay 服务创建订单时,不仅能够获得该笔交易的订单号,还会多一个 appAccountToken 参数用于后续使用。
相较于 applicationUsername,在 StoreKit 2 中发起购买时,可以带上 appAccountToken 参数。AppAccountToken 使用 UUID 格式,它将永久保存到交易的 Transaction 信息中,有了该信息,可以方便地处理后续用户的补单,退款等操作。
# StoreKit 1
func purchase(_ product: SKProduct) {
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)
}
extension StoreKitService: SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions:
transactions.forEach { transaction in
switch transaction.transactionState {
case .purchased:
// handle purchased
case .failed:
// handle failed
case .deferred:
// handle deferred
case .restored:
// handle restored
default: break
}
}
}
# StoreKit 2
func purchase(_ product: Product) async throws -> Transaction? {
let result = try await product.purchase(options::[.appAccountToken(yourAppToken))])
switch result {
case .success(let verification):
// handle success
return result
case .userCancelled, .pending:
// handle if needed
default: break
}
帐单验证
在 StoreKit 1 中,我们需要在支付结束上传整串的 receipt data 给到服务端用于校验。
而在 StoreKit 2 中,Apple 使用 JWS 存储 transaction 信息,且 transaction 的校验会由 StoreKit 2 完成。这个时候,我们只需要上报交易时的 transaction Id 给到服务端,服务端通过 transaction Id 进行后续处理即可。
在正常情况下,一笔订单在经过上述几步后已经完成。
由于网络等其他特殊情况的发生,会出现用户已经扣款,但是没有完成订单验证和发货的情况。在 StoreKit 1 中, Apple 提供了 SKPaymentTransactionObserver 用于监听 transaction 的更新。在 StoreKit 2 中,我们可以按照如下进行交易更新的监听:
# StoreKit 2
func listenForTransactions() -> Task.Handle<Void, Error> {
return detach {
for await result in Transaction.updates {
do {
// handle transaction result here
}
}
}
}
支持 StoreKit 2 过程中的一些问题
虽然 StoreKit 2 大大简化了 iOS 端整个支付服务的开发成本,但是在实际开发中同样遇到了一些问题。
问题 1:
在 iOS 15.4 之前,Transaction.updates 无法监听到 transaction 更新信息。
解决方案: 在 iOS 15.4 之前使用 StoreKit 1 提供的
SKPaymentTransactionObserver 处理 transaction 的更新情况。
问题 2: StoreKit 2 未提供来自 App Store 用户直接购买的回调方法
解决方案: 仍然使用 StoreKit 1 提供的 API 完成事件的响应。
func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool
问题 3: StoreKit 2 未提供获取商品价格的本地化 currencySymbol
解决方案:
if let price = product.price.formatted(), let displayPrice = product.displayPrice {
let currencySymbol = displayPrice.replacingOccurrences(of: price, with: "") // with StoreKit2, apple doesn't provide method to get currencySymbol directly
}
问题 4: StoreKit 1 和 StoreKit 2 的兼容性
在流利说的背景下,需要同时支持 StoreKit 1 和 StoreKit 2。在代码改造中,我们根据用户设备的 iOS 版本号(>= iOS 15.0)来初始化不同的 In-App Purchase 服务。经过测试,我们发现 StoreKit 1 能够顺利升级到 StoreKit 2。
通过模拟一台 iOS 12 运行 StoreKit 1 的设备进行一次交易,并未对 Transaction 进行 finish API 的调用。当设备升级到 iOS 16 后,通过 StoreKit 2 的 Transaction.updates API 仍然能够获取到该交易信息,并完成后续的交易验证和发货。
问题 5: 支持用户退款
在 StoreKit 2 中,我们可以直接使用 Transaction.beginRefundRequest 让用户针对某一笔交易申请退款。且这个链路,在沙盒环境中同样可以正常进行,后端能够收到来自 Apple 的退款通知。(但是不会和线上环境一样收到 Apple 的邮件)
问题 6: 订单同步
在 StoreKit 1 中通过 SKReceiptRefreshRequest 来更新当前用户的最新账单信息,在 StoreKit 2 中,Apple 提供了 await AppStore.sync() 用于账单信息更新。但这在 StoreKit 2 中不是必须的。通常情况下 StoreKit 2 会自己完成跨设备的交易同步,当用户重新安装 App 时,StoreKit 同样会同步所有的交易信息。即便如此,我们仍然需要在 App 的显著地方提供 Restore 的能力,否则不会通过 Apple 的审核。
问题 7: 通过 Apple 订单号找回
感谢 StoreKit 2 ,我们可以通过 Apple 订单号完成用户的交易信息确认。但是在沙盒环境下,这个功能无法进行测试。
展望
StoreKit 2 解决了绝大部分 StoreKit 1 中遇到的问题。由于 Apple 的重新设计,在 iOS 端和后端接口都有了很大的变化,本文仅仅梳理出了部分开发过程中遇到的问题。
建议读者在打算升级 StoreKit 2 时,详细阅读 Apple 文档: https://developer.apple.com/documentation/storekit/
欢迎一起交流 !
原文始发于微信公众号(流利说技术团队):iOS 篇: 流利说 iOS App 支持 Apple In-App Purchase StoreKit 2
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论