花了快10个工作日,终于完成了内购(IAP)功能。必须写篇文章来记录一下这十天来的心得体会,更是为了避免后续的开发者重复入坑。
网上关于IAP的文章已经有很多了,我基本全都看了一遍,问题太大了。首先不说这些文章里面的内容大同小异,很多文章都是直接照抄其他文章的,就说很重要的一点,很少有文章提到移动端在内购完成后,如何通过自己的服务器向苹果后台验证此次交易的正确性。大多数的文章都在千篇一律的教你怎么去后台配置商品,怎么配置财务信息,怎么配置测试账号这些东西,然后放上大同小异的代码就算完成一篇博文了。此处只有呵呵二字......
首先我希望读到这篇文章的你至少已经学会了如何在iTunes Connect进行IAP的配置。因为本文不会再去说这些没有什么技术含量的配置流程问题。如果你不知道,请移步:
http://yimouleng.com/2015/12/17/ios-AppStore/
这是所有我看过的IAP文章中最好的一篇了,跟着这篇文章走,你会成功的配置好IAP。
本文要谈的三点内容:
- 获取不到商品信息的原因。
- 从获取到商品信息开始到苹果后台返回成功购买的信息中间发生了什么。
- 如何搭建自己的服务器向苹果后台验证交易的正确性。
** 第一点:获取不到商品信息 **
当你兴致冲冲的做完那些配置后,调试代码你很可能会发现请求到的商品列表为空。然后你会以为自己哪里没有配置正确,又把相关的配置博文翻来覆去的看几遍,最后很失望的发现然并卵。这里,我根据自己的经验结合网上的一些答案给大家做个总结:
- 确定配置环节正确。
- 确定是真机测试且手机没有越狱。
- 确定内购商品添加到了需要内购功能的App中。
- 确定当前运行的App的Bundle ID和后台配置的App的Bundle ID是一致的。
- 可以尝试先删除旧App,再重新编译生成新的,避免新App未覆盖错误。
这里要提一点,沙盒的测试账号和你请求商品信息没有关系。请求商品信息的流程是,你在后台配置好了内购商品,并且将其添加到了需要集成内购功能的App中,然后你请求商品。请求到商品后的流程是这样的,苹果系统会自动弹出登录框让你登录账号。然后根据提示操作进行购买,这里的账号就是你配置的沙盒测试账号。
如果你做到了以上五点,还是获取不到商品信息。欢迎评论中留言提出问题,我会尽量帮助大家解决。
** 第二点:从开始获取商品信息到苹果后台返回成功购买的信息中间发生了什么 **
(这里只贴出核心代码讲解,带着大家理顺购买商品的整个思路,我会在文章末尾给出一些不错的封装好的工具类推荐给大家,因为思路清晰之后,你才能真正的理解IAP的购买机制,我们应该做到理解而不仅仅是会用)
在第一点中我已经说了付费购买的环节和请求商品信息的环节是相互独立的,分别通过不同的API去调用完成,并通过不同的代理和监听方法返回信息。但这里有一个顺序性,你只有请求到商品信息后,并且将商品信息发送到苹果后台(此时商品信息一定有效,因为你已经请求到了商品信息),才开始付费购买的环节。正常情况下,当需要购买的商品信息发送到苹果后台后,系统会自动弹出账号登录框,这个时候在测试环境下你需要输入沙盒测试账号,在上线版本中,需要输入用户绑定的苹果账号,接下来都是系统弹窗,按照提示购买就行了。
整个购买过程我们需要用到苹果的一个库
#import <StoreKit/StoreKit.h>
1.请求商品信息 这里需要用到SKProductsRequestDelegate,SKProductsRequestDelegate是商品请求回调,用来告诉你有没有这个商品
#pragma mark - 请求商品信息
//请求商品
- (void)requestProductData:(NSString *)type{
NSLog(@"-------------请求对应的产品信息----------------");
[SVProgressHUD showWithStatus:@"请求产品信息中" maskType:SVProgressHUDMaskTypeBlack];
NSArray *product = [[NSArray alloc] initWithObjects:type,nil];
NSSet *nsset = [NSSet setWithArray:product];
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
request.delegate = self;
[request start];//开始请求
}
//收到商品返回信息,并将其包装成SKPayment,发送购买请求。
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
NSArray *product = response.products;
//没有商品
if([product count] == 0){
[SVProgressHUD dismiss];
return;
}
//有商品 发送购买请求
SKProduct *p = nil;
for (SKProduct *pro in product) {
p = pro;
}
SKPayment *payment = [SKPayment paymentWithProduct:p];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
SKProductsRequest是苹果封装好的一个对象。该对象有两个属性,products是一个数组,代表的是你获取到的所有商品信息,每个商品都是一个数组元素。invalidProductIdentifiers是无效的商品id的数组,此id对应的是你在苹果后台构建的商品id。如果你调试代码的过程中发现,product为nil,invalidProductIdentifiers有值,那请回到第一步,因为你未请求到商品。
// Array of SKProduct instances.
@property(nonatomic, readonly) NSArray<SKProduct *> *products NS_AVAILABLE_IOS(3_0);
// Array of invalid product identifiers.
@property(nonatomic, readonly) NSArray<NSString *> *invalidProductIdentifiers NS_AVAILABLE_IOS(3_0);
2.判断购买结果,这里需要用到SKPaymentTransactionObserver,SKPaymentTransactionObserver是交易观察者,用来告诉你交易进行到哪个步骤了。
//监听购买结果 购买顺序:商品添加进列表
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction{
for(SKPaymentTransaction *tran in transaction){
switch (tran.transactionState) {
case SKPaymentTransactionStatePurchased://交易成功
[self completeTransaction:tran];
break;
case SKPaymentTransactionStateFailed://交易失败
[self failedTransaction:tran];
break;
case SKPaymentTransactionStatePurchasing://商品添加进列表
break;
case SKPaymentTransactionStateRestored://已购买过该商品
break;
case SKPaymentTransactionStateDeferred://交易延迟
break;
default:
break;
}
}
}
**第三点:交易成功后,向苹果后台验证 **
//交易结束
#pragma mark - **************** Private Methods
- (void)completeTransaction:(SKPaymentTransaction *)transaction {//交易成功
NSURL *receiptUrl=[[NSBundle mainBundle] appStoreReceiptURL];//这里的URL测试环境下为沙盒url,上线版本中应为苹果后台的URL
NSData *receiptData=[NSData dataWithContentsOfURL:receiptUrl];
NSString *receiptString=[receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];//转化为base64字符串
// 向自己的服务器验证购买凭证(此处应该考虑将凭证本地保存,对服务器有失败重发机制)
/**
服务器要做的事情:
接收ios端发过来的购买凭证。
判断凭证是否已经存在或验证过,然后存储该凭证。
将该凭证发送到苹果的服务器验证,并将验证结果返回给客户端。
如果需要,修改用户相应的会员权限
*/
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
这里说的购买凭证就是receiptString。将这个作为参数传给自己的服务器,让自己的服务器去向苹果后台验证。这里说一下,有的公司做的比较好,直接让移动端传交易的订单号给服务器。这个其实都一样,看各位公司的后台怎么搭建了。我们公司的后台没做过这方面,我们是在共同阅读苹果官方文档的情况下,自己搭建出来的接口,苹果会给你返回一串信息。其中会把所有的历史购买记录返回给你,注意,这些购买记录是无序的,需要后台通过时间戳将其排序出来,这样后台才能拿到最新的请求购买信息去做验证(这是我们调试了几个小时得出来的结论,满满的干货!)
官方文档的说明对receiptString的获取。并且要求你将它发送给自己的服务器。
我稍微提一下后台接口设计的要点。自己服务器像苹果后台去请求验证信息的时候,设计的字段为receipt-data。这是官方文档中写明的。receipt-data对应的字段值就为上面提到的receiptString
苹果后台收到服务器发过来的验证请求后会返回以下字段,status代表此次交易的状态,receipt代表凭证信息,是一个数组,里面是每一次交易的历史记录。
这是status返回的状态码,对应的值都有解释,看英文不好的同学可以谷歌翻译一下哈。
关于receipt的每一个JSON对象里包含的字段及对应信息说明,因为太多,这里就不贴图一一分析了。需要说明一点的是,这些历史记录是无序的,需要后台通过时间戳排序。
https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html#//apple_ref/doc/uid/TP40010573-CH106-SW1
这里需要注意的一点是,如果你发现请求到了商品信息,也发送了购买请求,但是监听购买结果的方法就是不执行。可以检查一下,是否在工具类初始化的时候,添加了监听。(这是我踩过的一个坑)
#pragma mark - 获取单例
+ (instancetype)sharedInstance{
static IAPPayManager* instance = nil;
static dispatch_once_t onceToken = 0;
dispatch_once(&onceToken, ^{
instance = [[IAPPayManager alloc] init];
[[SKPaymentQueue defaultQueue] addTransactionObserver:instance];//将工具栏对象添加为购买的监听对象
});
return instance;
}
最后附上几个作者在开发过程后参考的几个链接和一些不错的工具类:
http://www.cocoachina.com/special/iap.html(cocoachina上关于内购问题的整理)
https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html#//apple_ref/doc/uid/TP40010573-CH104-SW5(关于如何像苹果后台验证的官方文档)
https://github.com/mruegenberg/IAPManager(封装工具类)
https://github.com/saturngod/IAPHelper(封装工具类)
如果这篇文章真的帮助到了您,请顺手给个赞哈。