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

这一次为我们带来自己司 IAP
的实现过程详解,鉴于支付功效的重中之重以及错综复杂,著作会很长,而且付出注解的底细也波及至关首要,所以那个核心会含有三篇。

永不操心,我从没会只讲原理不留源码,我一度将我司的源码整理出来,你使用时只需要拽到工程中就可以了,下面开始咱们的情节

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

专注:文章中研讨的 IAP 是指使用苹果内购购买消耗性的档次。

笔者写了一个给 黑莓 X 去掉刘海的 APP,而且其他 中兴 也可以玩,有趣味的话去 App Store 看看。点击前往。

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;
}

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 中。

请记住这么些数量归档的层级,要不然,实现公文里看起来有些懵。

源码在此地。

您仍是可以够关心我要好维护的简书专题 iOS开发心得。这个专题的稿子都是实事求是的干货。假设您有问题,除了在篇章最终留言,还是可以够在网易 @盼盼_HKbuy上给自家留言,以及走访我的 Github

上一篇的剖析了 IAP
存在的题材,有九个点。假若您不亮堂是哪九个点,指出你先去看一下上一篇作品。现在大家依照上一篇总计的问题一个一个来对号入座解决。

08.还有什么问题?

到现在终结,第一篇上提及的两个问题,有四个在这一篇著作中都有相应的解决方案。由于篇幅原因,我就不大段大段的贴代码了,具体举行,肯定要看源码的,并且我写了巨细无比的注明,保证每个人都能看懂。

不过的确就从未问题了呢?不是的,现在已知的题目还有五个。

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

率先个问题,看起来要鸡蛋放在四个篮子里,比方说,数据要同时持久化到
keyChain
和沙盒中。不过这一次没有做,接下去看状态,如若实在有这种题材,可能会这样做。

其次个问题,是苹果 IAP
设计上的一个大的症结,看似无解,出现这种情况,也就是用户千方百计要阻拦交易成功,这只能他把苹果的订单邮件发给我们,我们手动给他加钱。

其它还有题目标话,请各位在评论区补充,一起谈谈,谢谢您的阅读!!

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 掉。

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

自家的篇章集合

下边这一个链接是本人抱有作品的一个会聚目录。这多少个作品凡是涉及实现的,每篇著作中都有
Github
地址,Github
上都有源码。

自家的著作集合索引

01.越狱的题目

有关越狱导致的问题,总是充满了不明了,每个人都不雷同,可是都是碰着了攻击导致的。所以,大家采用的办法大概粗暴,越狱用户一律不允许利用
IAP
服务。这里我也提出你如此做。我的源码中有一个工具类用来检测用户是否越狱,类名是
BLJailbreakDetectTool,里面唯有一个方法:

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

尽管你不想利用自家封装的法子,也得以动用友盟总括里有一个艺术,假若您的档次接入了友盟总括,你
#import <UMMobClick/MobClick.h> ,里面有个类形式:

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

05.品类布局总括

到现行终结,大家的构造早已有了大约了,现在我们来总计一下我们今日的花色协会。

BLPaymentManager 是交易管理者,负责和 IAP
通讯,包括商品查询和购进功用,也是交易情形的监听者,对接沙盒中收据数据的得到和换代,是我们整个支付的进口。它是一个单例,大家的声明队列是挂在它身上的。每当有新的贸易进入的时候(不管是咋样境况进来的),它都会把这笔交易丢给
BLPaymentVerifyManager,让 BLPaymentVerifyManager
负责去验证这笔交易是否行得通。最终,BLPaymentVerifyManager 也会和
BLPaymentManager 通讯,告诉 BLPaymentManager 某笔交易的景色,让
BLPaymentManager 处理掉指定的贸易。

BLPaymentVerifyManager
是认证交易队列管理者,它里面有一个需要验证的贸易 task
队列,它负责管理这么些队列的情状,并且驱动这么些任务的实践,保证每笔交易认证的程序循序。它的中间有一个
keyChain,它的连串中的任务都是从 keyChain
中开头化过来的。同时它也管理着keyChain 中的数据,对keyChain
举办增删改查等操作,维护keyChain 的情事。同时也和 BLPaymentManager
通讯,更新交易的状态(finish 某笔交易)。

keyChain
不用说了,负责交易数据的持久化,提供增删改查等接口给它的集团管理者使用。

BLPaymentVerifyTask 负责和服务器通讯,并且将通讯结果回调出来给
BLPaymentVerifyManager,驱动下一个表明操作。

04.压入新贸易

地点表达队列里本身还有压入情景没有解释,压入情景有两种情景。

第一种是出现意外,就是先导化的时候,尽管现身用户刚好交易完,不过 IAP
没有通告大家交易成功的动静,那么此时再去 IAP
的交易队列里检查几次,要是有没有被持久化到 keyChain 的,就一直压入
keyChain 中举办持久化,一旦进入 keyChain
中,那么这笔交易就能被正确处理,这种情形在测试环境下通常出现。

第二种是例行贸易,IAP 通告交易完成,此时将交易数据压入 keyChain 中。

其三种和率先系列似,用户从后台进入前台的时候,也会去反省一回沙盒中有没有没有持久化的贸易,一旦有,就把那些交易压入
keyChain 中。

下边六个压入情景,能最大程度上保证我们的持久化数据能和用户真正的贸易同步,从而预防苹果出现交易得逞却尚无打招呼我们而造成的
bug。