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

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

在意:文章中琢磨的 IAP 是指利用苹果内购购买消耗性的花色。

这一次为咱们带来自己司 IAP
的完成进度详解,鉴于支付作用的主要以及错综复杂,小说会很长,而且付出注解的细节也波及重大,所以这么些主旨会含有三篇。

第一篇:[iOS]贝聊 IAP
实战之满地是坑
,这一篇是支付基础知识的讲解,主要会详细介绍
IAP,同时也会相比较支付宝和微信支付,从而引出 IAP 的坑和注意点。
第二篇:[iOS]贝聊 IAP
实战之见坑填坑
,这一篇是高潮性的一篇,紧要针对第一篇小说中分析出的
IAP 的题目开展具体解决。
第三篇:[iOS]贝聊 IAP
实战之订单绑定
,这一篇是焦点的一篇,重要讲述小编探索将团结服务器生成的订单号绑定到
IAP 上的进度。

决不顾虑,我一贯不会只讲原理不留源码,我一度将我司的源码整理出来,你利用时只需求拽到工程中就可以了,上边伊始大家的内容

源码在那里。

上一篇的剖析了 IAP
存在的题材,有九个点。假诺您不明了是哪九个点,提出您先去看一下上一篇文章。现在我们根据上一篇统计的题目一个一个来对号入座解决。

作者写了一个给 索尼爱立信 X 去掉刘海的 APP,而且其余 索尼爱立信 也足以玩,有趣味的话去 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