iap内购实现解决方案:FGIAPService

4,996 阅读10分钟

GitHub 地址:FGIAPService 支持 cocopods,使用简便,效率不错的基础组件。

背景

不同于微信/支付宝支付的远程服务器做校验,IAP扣款后的交易验证是App驱动App服务器完成的。但是移动设备所处的网络环境远比服务端复杂;扣款成功后,后续的下发票据 和 App上传票据都面临严峻的考验(网络异常、App服务器异常、Apple服务器异常等)。

市场上已经存在的一些IAP的三方库,譬如RMStore、 IAPHelper等,相对来说也比较成熟,但为什么还需要另外写一个解决方案?

  1. 希望能耦合一定的订单业务从而更快更简单地接入项目
  2. IAP扣款后的交易验证是App驱动App服务器完成的,许多异常流程如果不能正确处理都会存在漏单风险
  3. 苹果推荐通过服务器做票据校验
  4. 持续性地维护

基本原理

iTunes配置以及创建商品的教程网上很多,就不再描述了,下面简单说说服务器票据校验流程

票据流程分为两种,一种是直接使用Apple的服务器进行购买和验证,另一种就是自己假设服务器进行验证。由于国内网络连接Apple服务器验证非常慢,而且也为了防止黑客伪造购买凭证,通用做法是自己架设服务器进行验证。下面是服务器验证流程

image.png

  • 用户进入购买虚拟物品页面,App从后台服务器获取产品列表然后显示给用户
  • 用户点击购买购买某一个虚拟物品,,APP就发送该虚拟物品的productionIdentifier到Apple服务器和自家服务器,分别生成SKProduct、tradeNo
  • 用户点击确认键购买该物品,并将购买请求发送到Apple服务器
  • Apple服务器完成购买后,返回用户一个完成的票据信息发送给到后台服务器验证
  • 后台服务器把这个凭证发送到Apple验证,Apple返回一个字段给后台服务器表明该凭证是否有效
  • 后台服务器把验证结果在发送到APP,APP根据验证结果做相应的处理

业务订单创建时机

网上关于业务订单号都是在选中商品就立即创建一个order,然后与商品进行绑定,最后在支付校验成功后删除绑定的订单信息。 但由于掺杂了订单的绑定逻辑,并且apple的applicationUsername信息再重新打开app的时候会丢失,所以目前大部分方案都是用applicationUsername结合Keychain去进行保存 order 信息。

然而不同的商品以及用户的不同操作,常常会造成会造成本地的订单DB表数据错乱,会出现各种各样的数据丢失、丢单的情况发生。

另外关于自动续费订阅,它的续费行为是apple自动发生的。还有apple在iOS11开始支持的 Promoting In-App Purchases,这2种付费方式是直接在用户打开App的时候自动触发 -paymentQueue:updatedTransactions:回调,自动完成支付。这就没办法再像前面说的,在选中商品的时候创建一个订单。

所以在我看来,最适合创建订单的时机应该是下面2个时刻:

  • 向服务器进行receipt校验的时候,完全交给后端去处理是否要创建一个订单(譬如自动订阅的第二次续费,需要后端通过originalTransaction.transactionIdentifier是否为空来判断更新或者创建新订单)
  • 收到用户支付成功回调时,根据Transaction数据在端上创建一个订单,然后push给服务器。这样逻辑会麻烦点,但很多公司后端才是大佬,干不过他们的时候只能妥协 (囧)。

一旦把订单的创建时机改成后面2种,业务订单order和apple的Transaction天然就存在了一对一的关系,就不需要另外创建一张DB表去维护。也自然不存在接下去要说的数据异常丢失的问题。

另外由于票据校验接口依旧可能失败,如果服务器要求每个创建的订单都必须close掉,建议将创建的order信息通过 Keychain来做持久化。

PS:关于用户扣费失败的事件,完全不需要创建一个order来污染订单表。如果想要记录支付成功率,完成可以通过点击支付、以及失败日志回调来进行统计

IAP支付的坑太多,这里是收集到的一些常见问题

窜单

订单映射指的是业务订单和IAP订单的映射,本质是将业务订单号 tradeNo绑定到苹果的交易订单(receipt)上

  • 在发起IAP支付后,我们给Apple的是一个SKPayment对象,最后监听到的是SKPaymentTransaction对象(有SKPayment属性对象);我们可以通过利用SKPayment的applicationUsername字段实现订单映射;
  • apple支付成功返回的票据receipt信息含有商品相关的字典信息,通过把tradeNo和receipt提交给服务器可以绑定
  • 通过id协议对外开放服务器校验接口
  • APP上切换账号:验证的时候是根据账号,根据订单来充钱

漏单

对于消耗型和非消耗型商品来说,没有finish的transaction就会出现在updatedTransactions函数里(订阅类型有没有finish都会出现)。常见就是在观察支付队列的函数里,不管什么状态先给finishTransaction,再自己造车轮搞一套本地存储和重发机制。经常在finishTransaction之后,自己造的车轮出了问题,造成丢单。

  • 建设交易验证队列;每笔交易数据持久化成功后,尝试订单验证,验证包含两步:上传票据 和 查询订单状态;只有两步都完成,才能算订单结束,执行 finishTransaction 操作;
  • 失败轮询:苹果支付成功但服务器校验失败的订单,会启动一个定时器做订单轮询
  • APP异常闪退:app一启动就会通过观察支付队列,处理未完成的transaction订单
  • 删除APP:app一启动就会通过观察支付队列,处理未完成的transaction订单

票据的问题

  • iOS 7后(App几乎都是iOS 9起步),从[[NSBundle mainBundle] appStoreReceiptURL]]中获得的receipt(票据)数据;App上传票据信息的话,将其中的数据一起上传;
  • iOS 7后,[[NSBundle mainBundle] appStoreReceiptURL]]中的票据信息是一个receipt list(in_app字段),本身带有“自动修复的特性”,如果用户某次支付没有正确完成,后续也没有被成功恢复;当他产生下一次成功支付后,[[NSBundle mainBundle] appStoreReceiptURL]]中会包含这几次支付的receipt。
  • 票据校验:将票据验证交给服务器去处理,通过请求环境判断是否是沙盒环境
PS: 票据数据一般都超过3000个字节,LLDB工作台的log日志有时候只会输出一部分,导致票据验证失败

数据异常丢失

  • applicationUsername丢失:通过applicationUsername来保存tradeNo有一定几率取出来为nil,为了保证能tradeNo不丢失,这里会把订单号tradeNo和productIdentifier持久化到keychain。交易数据持久化到keychain有两个好处:
    1. 存储到keychain的数据被加密,安全可靠;
    2. 即使App被卸载,keychain中数据也不会被删除;
  • 票据丢失:如监听到支付交易成功了,但是从[[NSBundle mainBundle] appStoreReceiptURL]]中获得的数据是空,遇到此类问题,可以打标记后,通过SKReceiptRefreshRequest重新获取票据数据。
  • transactionIdentifier丢失:支持流程通过订单映射完成,不需要考虑transactionIdentifier

Promoting In-App Purchases

前面提到iOS 11 之后,开发者可以在 App Store 自己App的下载页面推广自己的内购商品,用户可以直接在App下载页面购买内购商品,这就涉及到从App Store跳转到自己App,所以苹果在 SKPaymentTransactionObserver 新增了一个代理方法:

func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, 
for product: SKProduct) -> Bool {
    
    return false
}

用户如果在 App下载页面点击购买你推广的内购商品,如果用户已经安装过你的 App 则会直接跳转你的App并调用上述代理方法;如果用户还没有安装你的 App 那么就会去下载你的 App,下载完成之后系统会推送一个通知,如果用户点击该通知就会跳转到你的App并且调用上面的代理方法

上面的代理方法返回 YES 则表示跳转到你的 App,IAP 继续完成交易,如果返回 NO 则表示推迟或者取消购买,实际开发中因为可能还需要用户登录自己的账号、生成订单等,一般都是返回 NO,之后自己手动把代理方法里面返回的 SKPayment 加入支付队列,然后在按照自己的支付、验证逻辑完成支付。

自动续期订阅

iOS的4种内购类型,除了自动续期订阅外,通过上面这么实现就可以,但自动续期订阅有apple自动扣费、用户取消订阅、订阅升级这些额外的操作,也需要我们一一去测试。

譬如当前在同一个组内(一次只能选择组内的一个商品)创建2个续费型订阅 :连续包月连续包年

用户订阅了连续包月,再次选择订阅包月类型,会直接回调支付成功,返回transactionState = SKPaymentTransactionStatePurchased ;用户订阅了连续包月,接着又选择订阅包年类型,apple会提示升级,流程跟支付雷同。

当然啦,上面说的东西FGIAPService都已经帮你处理好了,只需要跟后端同事统一好 续费订单的处理方式,就可以放心享用啦 (#^.^#)。

值得注意的是,在沙盒环境的测试账号,自动订阅一般都在订阅5分钟后自动续费,订阅项目续期 12 次后将自动取消。如果需要修改或者中止,可以在https://appstoreconnect.apple.com/access/testers 中进行设置。

截屏2021-12-03 下午6.03.48.png

IAP周边建设

完善埋点

  • 支付环节中,对关键路径埋点,包括但不限于:持久化交易数据操作、上传票据操作、查询操作,结束验单操作等;
  • 监控异常的情况,包括但不限于:持久化交易数据失败、上传失败,applicationUsername获取订单号为空、查询失败等;
  • 开发阶段,尽可能多展示调试日志信息;

响应用户反馈

  • 再好的方案,也无法hold所有的IAP问题,建立用户反馈响应机制,及时响应用户反馈;
  • 票据无法正常获取、校验失败的订单将无法正常finish,所以需要手动帮助帮助用户解决问题后再关闭。
  • 预留ErrorCode:11000007,来删除本地过期订单 (一般不需要)

小结

由于内购直接涉及到公司盈利,需要加倍小心,所以在方案设计阶段,就调研了许多文章资料,包括三方库:RMStore、IAPHelper。分析其设计优点和缺点。

回顾开发调试过程,理解IAP整个支付链路的流程和实现机制,再到自己实现一个IAPService,文中说到的具体实现节,开发过程中也花费了不少时间去解决。

总体而言,得到了很好的锻炼,值得~ 有需要的朋友,赶快使用起来吧:项目地址

参考文章

iOS 内购(In-App Purchase)总结
谈谈苹果应用内支付(IAP)的坑
Apple IAP 二三事
苹果IAP开发中的那些坑和掉单问题