1. 前言
由于最近公司项目遇到一些问题,主要是测试时,要多次购买产生大量的transaction没有处理掉,导致产生大量的线程来处理,App hang住了。经过自己排查,发现是在validate receipt时候调用官方API SecStaticCodeCheckValidity时没有返回,导致transaction回调函数一直无法finished transaction。所以想写下这篇文章总结下一般的IAP过程,本文不会涉及具体的商品购买流程,只会介绍购买一个内购商品的基本过程以及主要的API介绍。如果想详细的了解IAP可以阅读这篇文章或者查看官方文档。
2. 查询内购商品及得到商品的信息
既然是要购买商品,当然需要商品信息。IAP的商品是注册在App Store Connect中的。如果需要可以参考这篇文档来了解App Store Connect的相关信息。那么你的App在内购商品时,需要向App Store你要购买的商品在不在,这样才能完成购买。你不可能购买一个不存在的商品,就无法提供给你相应的服务。
2.1 查询商品
当你查询的商品在时,App Store会返回给你的App相应的商品信息。 创建一个请求可以由如SKProductRequest完成,其中它可以带着查询商品的identifiers。查询商品的流程可以用下图表示:
假如你有两个要查询的商品的identiriers:productOne和productSecond。那么你创建一个SKProductRequest可以这样:
- (instancetype)initWithProductIdentifiers:(NSSet<NSString *> *)productIdentifiers;
NSArray *arrProductIdentifiers = @[@"firstProductName", @"secondProductName"];
NSSet *setProductIdentifiers = [NSSet setWithArray:arrProductIdentifiers];
SKProductRequest *productReq = [[SKProductRequest alloc] initWithProductIdentifiers:setProductInentifiers];
productReq.delegate = self; //处理从App Store 返回的Response的delegate
[productReq start];
当你创建了一个SKProductRequest对象,调用start方法。那么你的app就会向App Store请求内购商品信息。
2.2 执行代理方法获得response
想要得到你查询的商品信息,需要代理。代理需要符合协议SKProductsRequestDelegate。delegate method为:当App Store返回时,回执行该代理方法。该方法有两个参数,请求商品的Request和返回的Response。
- (void)productsRequest:(SKProductsRequest *)request
didReceiveResponse:(SKProductsResponse *)response;
request: The product request sent to the Apple App Store.
response: Detailed information about the list of products.
note: 该代理方法不会在一个指定的线程上执行。
返回的SKProductResponse的定义如下:
@interface SKProductsResponse : NSObject
其包含两个属性:products是你请求的商品的Array,invalidProductIdentifiers是App Store无法识别你请求的商品的identifier array,也就是你的app发送的请求商品信息,App Store无法识别。
@property(nonatomic, readonly) NSArray<SKProduct *> *products;
@property(nonatomic, readonly) NSArray<NSString *> *invalidProductIdentifiers;
2.3 SKProduct包含的信息
SKProduct的定义如下:
@interface SKProduct : NSObject
介绍如下三个属性(不止三个):价格,商品标志符,以及是否可以下载。
//商品的标志符,App Store根据这个查询商品是否存在。
@property(nonatomic, readonly) NSString *productIdentifier;
//本地价格,这个是因为不同地区的价格受汇率影响,所以不同国家和地区会有所不同,这个也包含货币符号。
@property(nonatomic, readonly) NSLocale *priceLocale;
//App Store是否改商品有内容可以下载至你的App安装的设备上。
@property(nonatomic, readonly) BOOL isDownloadable;
3. 处理交易
当你获得商品信息后当然是去购买,通过创建一个SKMutablePayment(继承至SKPayment)对象,来进行一次交易。一个SKMutablePayment对象里面包含了要购买的商品信息以及购买的数量,可以将它看作一个payment request,当交易处理结果后的交易(请注意这里不一定是指交易成功购满成功,后面会提到交易的状态),app store会给你的app发送transaction(已经购买成功的交易,会发送receipt和transaction identifier,你的app会永久保存这些)。 一个交易被处理的流程大致如下:
创建SKMutablePayment对象的方法定义如下:需要使用SKProduct对象,该对象从Response的products属性获得。
+ (instancetype)paymentWithProduct:(SKProduct *)product;
//创建一个购买商品为myProduct,数量为2的SKMutablePayment对象。
SKMutablePayment *myPayment = [SKMutablePayment paymentWithProduct: myProduct];
//quantity表示需要购买的商品数量。
myPayment.quantity = 2;
3.1 SKPaymentQueue
现在你的App有了交易对象,那么如何让App Store处理该交易呢?这时候就需要SKPaymentQueue,它的定义如下:
@interface SKPaymentQueue : NSObject
它的主要功能是:A queue of payment transactions to be processed by the App Store.被App Store处理的队列,队列中的元素为payment transactions。这时候可能有人疑惑了,transactions哪里来的? 事实上它可以通过如下代码创建:
[[SKPaymentQueue defaultQueue] addPayment:myPayment];
上面这段代码将上面创建的myPayment对象添加至defaultQueue中,当一个SKMutablePayment对象添加至SKPaymentQueue时,就会创建自动创建一个SKPaymentTransaction对象(SKPaymentTransaction 对象有一个属性就是SKMutablePayment),并将其添加至SKPaymentQueue中。 SKPaymentQueue负责与App Store的通信,以及购买付款的界面。购买界面可以如下图所示:这些购买提示页面也是SKPaymentQueue负责的。
SKPaymentQueue是持久存在的,无论你的App打开再关闭,然后再打开,它能保持关闭前的状态。
3.2 addPaymentTransactionObserver
当然只是将SKPayment添加至SKPaymentQueue是不够的,还需为SKPaymentQueue添加一个符合SKPaymentTransactionObserver协议的对象。 你的App需要在启动时,就需要为SKPaymentQueue添加一个符合SKPaymentTransactionObserver协议的对象,如果没有,则SKPaymentQueue就无法与App Store同步transaction的状态。当一个交易完成购买了,queue更新transaction的状态,并且通知Observer调用相应的回调函数来处理逻辑,在这部分逻辑中,你将其从queue中移除。 添加Observer的方法如下:
//方法定义
- (void)addTransactionObserver:(id<SKPaymentTransactionObserver>)observer;
//self 是符合SKPaymentTransactionObserver协议的对象。
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
当然一个queue可以不止一个对象,你也可以为queue添加多个Observer。
3.3 Oberver的回调函数
一个符合SKPaymentTransactionObserver协议的类实现的回调方法,用以处理App Store处理后的SKPaymentTransaction。其中一个
- (void)paymentQueue:(SKPaymentQueue *)queue
updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions;
queue:为与App Store进行通信的SKPaymentQueue
transactions:在queue中被App Store处理完的SKPaymentTransaction Array。
你可以在该回调函数中,对完成购买的交易(根据transaction的状态),将其从queue中移除。如果一个transaction没有被移除,则它一直存在于你的SKPaymentQueue中,那么App Store会一直处理该transaction,也就是上面的回掉函数会一直被调用。(这也就是我负责的app hang住的原因)。
- (void)finishTransaction:(SKPaymentTransaction *)transaction;
[SKPaymentQueue defaultQueue] finishTransaction: finishedTransaction];
3.4 transaction state
transaction 对象有一个属性叫做transactionState,其定义如下:
@property(nonatomic, readonly) SKPaymentTransactionState transactionState;
其值有如下几种:
- 正在购买中。
- 成功购买。
- 购买失败。
- 之前购买的恢复成功。
- 购买处于待定状态,比如小孩子购买,需要家长同意。
//A transaction that is being processed by the App Store.
1. SKPaymentTransactionStatePurchasing
//A successfully processed transaction.
2. SKPaymentTransactionStatePurchased
//A failed transaction.
3. SKPaymentTransactionStateFailed
//A transaction that restores content previously purchased by the user.
4. SKPaymentTransactionStateRestored
//A transaction that is in the queue, but its final status is pending external action such as Ask to Buy.
5. SKPaymentTransactionStateDeferred
4. validReceipt
当transaction的状态为SKPaymentTransactionStatePurchased时,App Store会将receipt发送给你的App。那么对其进行验证之后,才能将transaction从PaymentQueue中移除。 了解validReceipt需要一些安全编码技术(可怜我木有,后面慢慢补吧,所以这一部分代码没有看懂项目中的!)。validReceipt主要有两种方式,你可以选择两种都采用,也可以只采用一种,这取决于你。
- 本地设备验证 最适合验证IAP购买的商品的receipt
- App Store服务器验证,适合需要持久化IAP的购买记录和管理内容的receipt 本小节简单介绍下App Store服务器validReceipt,首先App Store Receipt是使用Apple证书签名的二进制加密文件。如果想要读取内容,你需要通过verifyReceipt。 valid receipt的过程可以简单描述为如下图所示:
4.1 Fetch the Receipt Data
为了得到receipt data,需要通过appStoreReceiptURL,这个URL代表receipt的路径。然后将其编码成Base64,最后发送该编码数据到你的Server上。代码可写成如下逻辑:
/* Load the receipt from the app bundle. */
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
if (!receipt) {
NSLog(@"no receipt");
/* No local receipt -- handle the error. */
} else {
/* Get the receipt in encoded format */
NSString *encodedReceipt = [receipt base64EncodedStringWithOptions:0];
}
/* ... Send the receipt data to your server ... */
4.2 Send the Receipt Data to the App Store
首先你的Server创建一个JSON对象,该对象是哪个内容
- receipt-data(byte) The Base64-encoded receipt data
- password(string) 你的app的共享密钥,可在App Store Connect中生成。是一个十六进制的String,想了解更多,可以点击Generate a Receipt Validation Code
- exclude-old-transactions(boolean) 如果该值设置为true,则将JSON发送给App Store只会返回最近的订阅续订交易,这个字段仅仅用于包含自动续订的receipt。
将其作为Http Post Requset的body。不同的环境测试的URL不一样,测试环境使用
https://sandbox.itunes.apple.com/verifyReceipt,Product使用https://buy.itunes.apple.com/verifyReceipt。
4.3 Parse the Response
你的服务器向App Store发送一个Http Request,回返回一个response。会返回一个JSON对象,其中包含一些key及对应的value。
- environment(string) Sandbox or Production
- is-retryable(boolean) 你的server request出现错误,如果值为1表示是临时问题,可以后面进行此账单的验证,值为0表示无法解决的问题,不要重复验证此收据。这种情况只适用于response的状态码21100-21199
- latest-receipt(byte) 最新的Base64编码的receipt, 当receipt包含自动订阅的收据时,receipt才返回。
- latest_receipt_info(responseBody.Latest_receipt_info 包含改App所有应用内购买的数组,不包括已经被你应用finished的transaction。 当receipt包含自动订阅的收据时,才返回此参数。
- pending_renewal_info(responseBody.Pending_renewal_info) 在JSON文件中,是一个每一哥元素都包含product_id标志的自动续订订阅的待续订信息。当receipt包含自动订阅的收据时,才返回此参数。
- receipt(responseBody.Receipt) JSON对象,代表被发送去验证的receipt。
- status 如果收据有效则为0,如果有错误则为状态码,状态码表示整个app receipt的状态。想了解更多状态码的信息,请阅读状态码描述 根据返回的信息,可以对收据是否验证成功进行判断。如果验收成功,那么你的服务器就可以向你的App发送购买的商品。服务器就可以通知你的App结果,根据服务端的购买成功的结果,将相对应的已经为finished状态的transaction从SKPaymentQueue中移除,这样IAP商品购买就成功完成了。
5. 总结
- 在IAP流程中,涉及到三个角色,Your App,Your Server and App Store。
- 查询商品阶段只是Your App 和 App Store通信。
- 购买商品阶段涉及三个角色。Your App 添加购买商品交易,在SKPaymentQueue中生成transaction,SKPaymentQueue与App Store进行通信。完成付款之后。Your App收到App Store返回的receipt。Your App发送receipt指Your Server,然后Your Server将发送Hppt Request与App Store进行通信,得到App Store的Response。根据Response通知Your App receipt验证成功,并发送购买的商品。Your App根据Your Server的购买成功的结果,将相应的transaction从SKPaymentQueue中移除,这样一个商品的IAP购买就完成了。