iOS 篇: 流利说 iOS App 支持 Apple In-App Purchase StoreKit 2

admin 2023年1月7日15:53:04移动安全评论10 views5078字阅读16分55秒阅读模式

背景

iOS 篇: 流利说 iOS App 支持 Apple In-App Purchase StoreKit 2

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 正常购买链路大致如下:

  1. iOS 端使用 StoreKit 获取 In-App Purchase Product Id 对应的商品状态及详情

  2. iOS 端使用 Product Id 向流利说 Pay 服务发起创建订单的请求,获取该次交易的订单号

  3. iOS 端使用 StoreKit 向用户发起该笔商品的支付请求,并同时设置 payment 的 applicationUsername 为订单号等相关信息

  4. 用户完成支付,iOS 端获取支付对应的 receipts 上报后端进行验证,并带上交易的上下文信息(applicationUsername 等)

  5. 后端对 receipts 进行验证,验证通过后给用户发货并回调 iOS 端

  6. iOS 端获得 receipts 验证结果,该笔交易完成 

以下结合上述整个链路,通过 StoreKit 1 的视角带大家了解 StoreKit 2 如何进行适配及改造:

获取商品详情

在 StoreKit 2 中,获取商品详情的 API 大大简化,支持了 Swift Async/Await,一行代码就能搞定。

# StoreKit 1let productsRequest = SKProductsRequest(productIdentifiers: identifiers)productsRequest.delegate = selfproductsRequest.start()
self.productsRequest = productsRequest
extension StoreKitService: SKProductsRequestDelegate { func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { // 获取商品状态及信息 }}
# StoreKit 2let storeProducts = try await Product.request(with: identifiers)

创建订单及发起支付

在 StoreKit 2 的场景下,iOS 端向流利说 Pay 服务创建订单时,不仅能够获得该笔交易的订单号,还会多一个 appAccountToken 参数用于后续使用。

相较于 applicationUsername,在 StoreKit 2 中发起购买时,可以带上 appAccountToken 参数。AppAccountToken 使用 UUID 格式,它将永久保存到交易的 Transaction 信息中,有了该信息,可以方便处理后续用户的补单,退款等操作。

# StoreKit 1func purchase(_ product: SKProduct) {    let payment = SKPayment(product: product)    SKPaymentQueue.default().add(payment)}
extension StoreKitService: SKPaymentTransactionObserver { func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {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 2func 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 2func listenForTransactions() -> Task.Handle<VoidError> {    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

特别标注: 本站(CN-SEC.COM)所有文章仅供技术研究,若将其信息做其他用途,由用户承担全部法律及连带责任,本站不承担任何法律及连带责任,请遵守中华人民共和国安全法.
  • 我的微信
  • 微信扫一扫
  • weinxin
  • 我的微信公众号
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年1月7日15:53:04
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                  iOS 篇: 流利说 iOS App 支持 Apple In-App Purchase StoreKit 2 http://cn-sec.com/archives/1496297.html

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: