[iOS]贝聊 IAP 实战的见坑填坑

大家好,我是贝聊科技
iOS 工程师 @NewPan。

瞩目:文章中讨论的 IAP 是据以苹果内购购买消耗性的类。

这次为大家带来自己司 IAP
的实现过程详解,鉴于支付功能的要与错综复杂,文章会死丰富,而且出验证的细节为涉嫌重要性,所以是主题会包含三首。

第一篇:[iOS]贝聊 IAP
实战的满地是坑,这无异篇是出基础知识的教,主要会详细介绍
IAP,同时也会见对比支付宝和微信支付,从而引出 IAP 的坑和注意点。
第二篇:[iOS]贝聊 IAP
实战的见坑填坑,这同一首是高潮性的等同篇,主要针对第一首稿子中剖析出的
IAP 的题材进行具体解决。
第三篇:[iOS]贝聊 IAP
实战的订单绑定,这无异首是重头戏的同篇,主要讲述作者探索用自己服务器生成的订单号绑定到
IAP 上之过程。

绝不担心,我没会只有谈原理不养源码,我已经拿我司的源码整理出来,你以时止需要甩到工程中就是可了,下面开始我们的始末

源码在此地。

齐平等首之解析了 IAP
存在的题材,有九只点。如果您莫懂得凡是哪九个点,建议您先去看一下达等同首文章。现在咱们根据达同首总结的题材一个一个来对号入座解决。

作者写了一个叫 iPhone X 去丢刘海的 APP,而且其他 iPhone 也得玩玩,有趣味的语去 App Store 看看。点击前往。

01.越狱的题材

有关越狱导致的问题,总是充满了不显,每个人都非雷同,但是还是遭遇了抨击导致的。所以,我们下的方式大概粗暴,越狱用户一律不同意行使
IAP
服务。这里自己吗建议乃如此做。我之源码中生一个器类用来检测用户是否越狱,类名是
BLJailbreakDetectTool,里面仅来一个办法:

/**
 * 检查当前设备是否已经越狱。
 */
+ (BOOL)detectCurrentDeviceIsJailbroken;

如您不思量以自己包的措施,也可动用友盟统计里发出一个道,如果您的色接入了友盟统计,你
#import <UMMobClick/MobClick.h> ,里面有只像样方式:

/**
 * 判断设备是否越狱,依据是否存在apt和Cydia.app
 */
+ (BOOL)isJailbroken;

02.市订单的储存

达成同样首文章说交,苹果只会以交易成功之后通过
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions
通知我们交易结果,而且一个 APP
生命周期只通一致次,所以我们万万不能依赖苹果的这个主意来让收据的查询。我们要做的是,首先要苹果通知我们交易得逞,我们就要拿交易数额好存起来。然后再说然后,这样一来我们就可摆脱苹果通知交易结果一个生命周期只通一致蹩脚的噩梦。

那这样快的市收据,我们存在哪里吗?存数据库?存
UserDefault?用户同样推脱载 APP
就毛都没有了。这样的东西,只发生一个地方存太确切,那就是是
keychainkeychain 的性状就是是率先安全;第二,绑定 APP
ID,不见面丢掉,永远不会见扔,卸载 APP 以后重装,仍然能从 keychain
里恢复之前的数目。

吓,我们现在起计划我们的仓储工具。在初步前,我们只要使用一个叔正在框架
UICKeyChainStore,因为
keychain 是 C
接口,很不便用,这个框架对那个做了面向对象的包裹。我们现即使因这框架进行打包。

#import <UICKeyChainStore/UICKeyChainStore.h>
#import "BLWalletCompat.h"

NS_ASSUME_NONNULL_BEGIN

@class BLPaymentTransactionModel;

@protocol BLWalletTransactionModelsSaveProtocol<NSObject>

@optional

/**
 * 存储交易模型.
 *
 * @param models 交易模型. @see `BLPaymentTransactionModel`
 * @param userid 用户 id.
 */
- (void)bl_savePaymentTransactionModels:(NSArray<BLPaymentTransactionModel *> *)models
                                forUser:(NSString *)userid;

/**
 * 删除指定 `transactionIdentifier` 的交易模型.
 *
 * @param transactionIdentifier 交易模型唯一标识.
 * @param userid                用户 id.
 *
 * @return 是否删除成功. 失败的原因可能是因为标识无效(已存储数据中没有指定的标识的数据).
 */
- (BOOL)bl_deletePaymentTransactionModelWithTransactionIdentifier:(NSString *)transactionIdentifier
                                                          forUser:(NSString *)userid;

/**
 * 删除所有的 `transactionIdentifier` 交易模型.
 *
 * @param userid 用户 id.
 */
- (void)bl_deleteAllPaymentTransactionModelsIfNeedForUser:(NSString *)userid;

/**
 * 获取所有交易模型, 并排序.
 *
 * @return models 交易模型. @see `BLPaymentTransactionModel`
 * @param userid  用户 id.
 */
- (NSArray<BLPaymentTransactionModel *> * _Nullable)bl_fetchAllPaymentTransactionModelsSortedArrayUsingComparator:(NSComparator NS_NOESCAPE _Nullable)cmptr
                                                                                                          forUser:(NSString *)userid
                                                                                                            error:(NSError * __nullable __autoreleasing * __nullable)error;

/**
 * 获取所有交易模型.
 *
 * @param userid 用户 id.
 *
 * @return models 交易模型. @see `BLPaymentTransactionModel`
 */
- (NSArray<BLPaymentTransactionModel *> * _Nullable)bl_fetchAllPaymentTransactionModelsForUser:(NSString *)userid
                                                                                         error:(NSError * __nullable __autoreleasing * __nullable)error;

/**
 * 改变某笔交易的验证次数.
 *
 * @param transactionIdentifier 交易模型唯一标识.
 * @param modelVerifyCount      交易验证次数.
 * @param userid                用户 id.
 */
- (void)bl_updatePaymentTransactionModelStateWithTransactionIdentifier:(NSString *)transactionIdentifier
                                                      modelVerifyCount:(NSUInteger)modelVerifyCount
                                                               forUser:(NSString *)userid;

/**
 * 存储某笔交易的订单号和订单价格以及 md5 值.
 *
 * @param transactionIdentifier 交易模型唯一标识.
 * @param orderNo               订单号.
 * @param priceTagString        订单价格.
 * @param md5                   交易收据是否有变动的标识.
 * @param userid                用户 id.
 */
- (void)bl_savePaymentTransactionModelWithTransactionIdentifier:(NSString *)transactionIdentifier
                                                        orderNo:(NSString *)orderNo
                                                 priceTagString:(NSString *)priceTagString
                                                            md5:(NSString *)md5
                                                        forUser:(NSString *)userid;

@end

/**
 * 存储结构为: dict - set - model.
 *
 * 第一层 data, 是字典的归档数据.
 * 第二层字典, 以 userid 为 key, set 的归档 data.
 * 第二层集合, 是所有 model 的归档数据.
 */
@interface BLWalletKeyChainStore : UICKeyChainStore<BLWalletTransactionModelsSaveProtocol>

+ (BLWalletKeyChainStore *)keyChainStoreWithService:(NSString *_Nullable)service;

@end

NS_ASSUME_NONNULL_END

咱俩要保留之对象是
BLPaymentTransactionModel,这个目标是一个型,头文件如下:

#import <Foundation/Foundation.h>
#import "BLWalletCompat.h"

NS_ASSUME_NONNULL_BEGIN

@interface BLPaymentTransactionModel : NSObject<NSCoding>

#pragma mark - Properties

/**
 * 事务 id.
 */
@property(nonatomic, copy, nonnull, readonly) NSString *transactionIdentifier;

/**
 * 交易时间(添加到交易队列时的时间).
 */
@property(nonatomic, strong, readonly) NSDate *transactionDate;

/**
 * 商品 id.
 */
@property(nonatomic, copy, readonly) NSString *productIdentifier;

/**
 * 后台配置的订单号.
 */
@property(nonatomic, copy, nullable) NSString *orderNo;

/**
 * 价格字符.
 */
@property(nonatomic, copy, nullable) NSString *priceTagString;

/**
 * 交易收据是否有变动的标识.
 */
@property(nonatomic, copy, nullable) NSString *md5;

/*
 * 任务被验证的次数.
 * 初始状态为 0,从未和后台验证过.
 * 当次数大于 1 时, 至少和后台验证过一次,并且未能验证当前交易的状态.
 */
@property(nonatomic, assign) NSUInteger modelVerifyCount;

#pragma mark - Method

/**
 * 初始化方法(没有收据的).
 *
 * @warning: 所有数据都必须有值, 否则会报错, 并返回 nil.
 *
 * @param productIdentifier       商品 id.
 * @param transactionIdentifier   事务 id.
 * @param transactionDate         交易时间(添加到交易队列时的时间).
 */
- (instancetype)initWithProductIdentifier:(NSString *)productIdentifier
                    transactionIdentifier:(NSString *)transactionIdentifier
                          transactionDate:(NSDate *)transactionDate;

@end

NS_ASSUME_NONNULL_END

尽管是一对市的严重性信息。我们在此目标实现归档和解档的法后,就可拿这个目标归档成为平等段子
data,也足以打平段 data
中解档出这个目标。同时,我们得实现这个目标的 -isEqual:
方法,因为,因为我们在进行对象判等的时光,要进行局部重点信息之比对,来规定两单交易是否是平笔交易。代码太多矣,我就未贴贴了,细节尚待您自己下载代码进去看。

兹回 keyChain 上来。每个 BLPaymentTransactionModel
对象归档成一个 NSData,多个 data
组成一个会师,再以这个集归档,然后保留于一个盖 userid 为 key
的字典中,然后再次对字典进行归档,然后再保存到 keyChain 中。

央牢记这数归档的层级,要不然,实现公文里看起有点傻。

03.证实队列

及现在竣工我们好针对贸易数额进行仓储了,也就是说,一旦 IAP
通知我们有新的打响之交易,我们就把这笔交易有关的数据易成一个交易型,然后将此模型归档存到
keyChain,这样咱们尽管可知以证明数据的逻辑独立出来了,而非用依赖 IAP
的回调。

当今咱们开考虑怎么根据现已有的数据来齐传我们团结的服务器,从而使我们的服务器向苹果服务器的查询,如下图所显示。

咱们得设计一个排,队列里发出眼前用查询的交易 model,然后将 model
组装成一个 task,然后于这 task
中为我们的服务器发起呼吁,根据服务器返回结果再行发起下同样涂鸦呼吁,就是上图的叫方式
5
,这样形成一个闭环,直到这队列中装有的模型都被处理终结了,那么队列就高居休眠状态。

如首先糟让队列执行之有四栽状况。

第一种是初始化的时段,发现 keyChain
中尚产生没有出处理完得说明的市,那么这就是从头从 keyChain
动态筛有数初始化队列,初始化完事后,就好开于服务器发起验证请求了,也便是使得方式
1
。至于何以就是动态筛,因为此的天职有优先级,我们等会再说。

仲栽使任务履行之道是,当前行处于休眠状态,没有任务而履,此时用户发起购买,就会见一直以手上市放到任务队列中,开始向服务器发起验证请求,也尽管是令方式
2

其三种是用户从不曾网络交发网络的时刻,会失去对 keyChain
做同样糟检查,如果起没有起处理完毕的市,一样会为服务器发起呼吁,也便是使得方式
3

季种植是用户从后台进入前台的时刻,会错过对 keyChain
做相同不成检查,如果来没发生处理完的交易,一样会往服务器发起呼吁,也不怕是叫方式
4

来矣地方四种类型的触及验证的逻辑下,我们即便能够尽可怜程度确保所有的贸易还见面向服务器发起验证请求,而且是决不停歇的展开,直到所有的交易都印证了才会告一段落。

顷说于 keyChain
中获得多少有一个动态筛的操作,这是啊意思呢?首先,我们于服务器发起的证实,不自然成功,如果失败了,我们就要叫这个市型打及一个记,下次验证的时段,应该先验证那些并未被从及号的市型。如果无由标记,可能会见起一直以印证和一个市型,阻塞了别交易型的说明。

// 动态规划当前应该验证哪一笔订单.
- (NSArray<BLPaymentTransactionModel *> *)dynamicPlanNeedVerifyModelsWithAllModels:(NSArray<BLPaymentTransactionModel *> *) allTransationModels {
    // 防止出现: 第一个失败的订单一直在验证, 排队的订单得不到验证.
    NSMutableArray<BLPaymentTransactionModel *> *transactionModelsNeverVerify = [NSMutableArray array];
    NSMutableArray<BLPaymentTransactionModel *> *transactionModelsRetry = [NSMutableArray array];
    for (BLPaymentTransactionModel *model in allTransationModels) {
        if (model.modelVerifyCount == 0) {
            [transactionModelsNeverVerify addObject:model];
        }
        else {
            [transactionModelsRetry addObject:model];
        }
    }

    // 从未验证过的订单, 优先验证.
    if (transactionModelsNeverVerify.count) {
        return transactionModelsNeverVerify.copy;
    }

    // 验证次数少的排前面.
    [transactionModelsRetry sortUsingComparator:^NSComparisonResult(BLPaymentTransactionModel * obj1, BLPaymentTransactionModel * obj2) {

        return obj1.modelVerifyCount < obj2.modelVerifyCount;

    }];

    return transactionModelsRetry.copy;
}

04.遏制入新贸易

点说明队列里我还有压入情景没有讲,压入观有三种状况。

先是种是出现意外,就是初始化的上,如果起用户刚好交易截止,但是 IAP
没有打招呼我们交易完成的状,那么此时又失 IAP
的市队列里检查一整整,如果发没有发出被持久化到 keyChain 的,就一直抑制入
keyChain 中开展持久化,一旦上 keyChain
中,那么这笔交易就会叫正确处理,这种气象以测试环境下经常出现。

第二种植是常规交易,IAP 通知交易就,此时将市数据压入 keyChain 中。

其三种植和率先种植恍若,用户从后台进入前台的时候,也会失掉反省一全方位沙盒中生无发生无发生持久化的贸易,一旦出,就将这些交易压入
keyChain 中。

面三单压入情景,能最好酷程度上保险我们的持久化数据会同用户真正的市并,从而防止苹果出现交易成功倒从没通我们只要导致的
bug。

05.品种结构总结

及今毕,我们的布局就发生了盖了,现在咱们来总一下我们现的档次组织。

BLPaymentManager 是交易管理者,负责和 IAP
通讯,包括商品查询与进货功能,也是市状态的监听者,对接沙盒中收据数据的收获与翻新,是咱们全体支付的输入。它是一个单例,我们的验证队列是挂于她身上的。每当发生新的市上的时段(不管是什么状况进来的),它还见面管这笔交易丢给
BLPaymentVerifyManager,让 BLPaymentVerifyManager
负责夺验证这笔交易是否管用。最后,BLPaymentVerifyManager 也会和
BLPaymentManager 通讯,告诉 BLPaymentManager 某笔交易的状态,让
BLPaymentManager 处理掉指定的贸易。

BLPaymentVerifyManager
是印证交易队列管理者,它里面生一个内需征的市 task
队列,它负责管理这些队列的状态,并且使这些任务之履,保证每笔交易证的主次循序。它的其中有一个
keyChain,它的阵中的天职还是起 keyChain
中初始化过来的。同时她吗管理方keyChain 中的数目,对keyChain
进行增删改查等操作,维护keyChain 的状态。同时为与 BLPaymentManager
通讯,更新交易的状态(finish 某笔交易)。

keyChain
不用说了,负责市数据的持久化,提供增删改查等接口给她的领导者使用。

BLPaymentVerifyTask 负责和服务器通讯,并且用通讯结果回调出来为
BLPaymentVerifyManager,驱动下一个证操作。

06.收据不联合处理

发同行报告说,IAPbug,这个 bug
就是判通知交易就成功了,但是去沙盒中得收据时,发现收据为空,这个题材吗是一旦实际回复之。

本召开了以下的拍卖,每次和后台通讯的结果归为三类,第一近乎,收据有效,验证通过;第二接近,收据无效,验证失败;第三类,发生错误,需要重验证。每个
task 回来还是一味生或是随即三种植状况的一律种,然后 task
的回调会让班管理者,队列管理者会拿回调传出去吃市管理者,此时市管理者在下面的代办方被创新最新的收据,并拿新收据还传为班管理者,队列管理者下次发起呼吁虽是运用新型的收据进行说明操作。

@protocol BLPaymentVerifyTaskDelegate<NSObject>

@required

/**
 * 验证收到结果通知, 验证收据有效.
 */
- (void)paymentVerifyTaskDidReceiveResponseReceiptValid:(BLPaymentVerifyTask *)task;

/**
 * 验证收到结果通知, 验证收据无效.
 */
- (void)paymentVerifyTaskDidReceiveResponseReceiptInvalid:(BLPaymentVerifyTask *)task;

/**
 * 验证请求出现错误, 需要重新请求.
 */
- (void)paymentVerifyTaskUploadCertificateRequestFailed:(BLPaymentVerifyTask *)task;

@end

07.注意点

  • 自从 iOS 7
    开始,苹果之收据不是每笔交易一个收条,而是以有着的贸易收据组成一个聚众在沙盒中,然后我们在沙盒中得到之收据是眼前具备收据的聚集,而且我们也未知情当前收据里还发哪些订单,我们的后台也无亮堂,只有
    IAP
    服务器知道。所以,我们不要管收据里之数,只要拿出来怼给后台,后台还怼给苹果就是好了。

  • 对此咱们交给后台的收据,后台可能会见召开过的号子。但是后台要判时之这收据是否之前都达污染了了,这时我们好开一个
    MD5,我们拿 MD5 的结果共同上传给服务器。

  • 类型里开了诸多报警的处理,比方说咱俩把收据存到 keyChain
    中,存储完成以后,要举行相同次于检查,检查是数据确实是怀着上了,如果没,那这当报警,并将报警音达传播我们的服务器,以防出现意外。又使说,IAP
    通知我们交易成功,我们就会见去得收据,如果此时收据为空,那绝对有问题了,此时应该报警,并将报警音上传(项目里曾经针对这种情景开展了容错)。还有按某笔交易证了几十次等,还是不许证实,那这应该设定一个证实次数之报警阈值,比方说十差,如果超过十软就是报警。

  • 当持久化到 keyChain 时,数据是绑定用户 userid
    的,这一点吧是重点,要不然会冒出 A 用户之交易以 B 用户那里证实。

  • 对此已破产了之印证请求,每半次等呼吁中的日子增长率也是该考虑的。这里运用的比较简单的方,只要是就同后台验证了同时失败了之交易,
    两浅呼吁中的日间隔是
    失败的次数 * BLPaymentVerifyUploadReceiptDataIntervalDelta。同时也本着步长的绝特别价值做了限定,防止步长越来越老,用户体验差。

  • 再有一些细节,下面两独道肯定要以遵循要求调用,否则后果非常严重。下面的亚独方法,如果用户已等录,重新开动的时刻吧使调用一次。

/**
 * 注销当前支付管理者.
 *
 * @warning ⚠️ 在用户退出登录时调用.
 */
- (void)logoutPaymentManager;

/**
 * 开始支付事务监听, 并且开始支付凭证验证队列.
 *
 * @warning ⚠️ 请在用户登录时和用户重新启动 APP 时调用.
 *
 * @param userid 用户 ID.
 */
- (void)startTransactionObservingAndPaymentTransactionVerifingWithUserID:(NSString *)userid;
  • 还有一个题目,如果用户眼前尚出未获印证的交易,那么此时客脱离登录,我们应该叫个
    UI 上之提醒。通过下这法子去用用户眼前是不是发生免获取认证的交易。

/**
 * 是否所有的待验证任务都完成了.
 *
 * @warning error ⚠️ 退出前的警告信息(比如用户有尚未得到验证的订单).
 */
- (BOOL)didNeedVerifyQueueClearedForCurrentUser;
  • 再有对于开发是串行还是并行的选取。串行的意是使用户眼前来无得的交易,那么就算无同意开展选购。并行的意思是,当前用户产生非就的市,仍然可以展开采购。我提供的源码是永葆彼此的,因为马上计划之时光就考虑到之问题了。事实上,苹果对同一个交易标识的活之购是串行的,就是若眼前产生非付款成功的货
    A,当您再度买入是商品 A
    的当儿,是免克进成功的。我们最后兼顾后台的逻辑,为了吃后台同事更加有利于,我们利用了串行的方法。采用串行就会见带一个逻辑漏洞就是,假如有用户他请后出现异常,导致无法利用正规的法门充钱并且
    finish
    某笔交易,最后经过跟咱们客服联系的艺术手动充钱,那么他的钥匙链就径直有平等画不形成的贸易,由于我们的购置时串行的,这样会招这用户更为没法请活。这种状态也是索要警醒的,此时独待同后端同时约定一下,再次应验这笔订单的早晚回来一个错误码,把这笔订单特别之
    finish 掉就好了。

  • 再有一个 IAP 的 bug,就是 IAP
    通知交易就,然后我们拿市数据存起来去后台验证,验证成功以后,回到
    APP 使用 transactionIndetify 从 IAP
    未成功交易列表中取出对应之交易,将及时较交易 finish 掉,当 IAP 出现
    bug
    的时节,这个交易找不顶,整个未到位交易列表都为空。而且复现也杀简短,只要在弱网下市得逞这杀掉
    APP
    就好复现。所以我们得承诺本着之问题。应对的国策就是给我们囤的数额加以一个态,一旦出现验证成功返回
    finish 的时光找不交相应之贸易,就先为存储数据加以一个
    flag,标识这笔订单已经证明了了,只是还未曾找到呼应的 IAP 交易进行
    finish,所以随后每次从未说明交易里取得多少的时,都急需将出此
    flag 的贸易对比一下,如果出现就认证了之市,就直接以那无异笔交易
    finish 掉。

08.还起怎么样问题?

到今日寿终正寝,第一篇上提及的八单问题,有七只在当下无异首文章被都有对应之解决方案。由于篇幅由,我便非坏截大段的贴代码了,具体实践,肯定使拘留源码的,并且我勾勒了巨细无比的注解,保证每个人且能看懂。

只是确就从未问题了吗?不是的,现在曾领略的题材还有少数只。

  • 没验证完, 用户更换了 APP ID, 导致 keychain 被更改。
  • 订单没有以到收据, 此时用户更换了手机, 那么此时收据肯定是以不交之。
  • ……

第一独问题,看起而鸡蛋在两单篮子里,比方说,数据而又持久化到
keyChain
和沙盒中。但是这次没有召开,接下去看状态,如果实在发这种问题,可能会见如此做。

老二个问题,是苹果 IAP
设计达到之一个大之老毛病,看似无解,出现这种状况,也就是用户千方百计使阻拦交易成功,那只好他将苹果之订单邮件发给我们,我们手动于他加钱。

另还有问题吧,请各位在评论区补充,一起谈谈,谢谢你的阅读!!

本人之章集合

下这个链接是本身有所文章的一个凑合目录。这些文章是涉及实现之,每篇文章中还发
Github
地址,Github
上还发生源码。

我的篇章集合索引

而还可关心我要好维护的简书专题 iOS开发心得。这个专题的文章还是实在的干货。如果你产生题目,除了当篇章最后留言,还得在微博 @盼盼_HKbuy上叫我留言,以及走访我之 Github。

发表评论

电子邮件地址不会被公开。 必填项已用*标注