iOS 内购IAP(In-App Purchases)代码实现(下)
上次介绍了苹果内购的交易流程,接下来讲讲获取票据信息和防止漏单。
为什么要获取票据信息?票据信息是苹果返回给我们的购买凭证。我们可以拿这个凭证,到苹果服务器上去验证真伪,从而确定是否给用户发放商品。
一般验证票据的工作,要放到服务器上面去做,这样才能确保不会被人破解,造成不必要的损失。
获取票据信息
当系统响应交易队列回调的时候,即paymentQueue: updatedTransactions: 被调用,我们可以获得一个SKPaymentTransaction参数。通过SKPaymentTransaction参数,我们可以获取苹果返回来的票据。
下面方法使用SKPaymentTransaction来获取票据信息:
#pragma mark - 获取票据信息
- (NSData*)receiptWithTransaction:(SKPaymentTransaction*)transaction {
NSData *receipt = nil;
if ([[NSBundle mainBundle] respondsToSelector:@selector(appStoreReceiptURL)]) {
NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
receipt = [NSData dataWithContentsOfURL:receiptUrl];
} else {
if ([transaction respondsToSelector:@selector(transactionReceipt)]) {
//Works in iOS3 - iOS8, deprected since iOS7, actual deprecated (returns nil) since iOS9
receipt = [transaction transactionReceipt];
}
}
return receipt;
}
获取票据的方式有两种。一种是直接获取SKPaymentTransaction里面的属性transactionReceipt。这种方式,在iOS7已经废弃了,到iOS9停用。但是为了兼容旧机型,我们还是加上这个方式。
一种是使用[[NSBundle mainBundle] appStoreReceiptURL],这种方式是最新的方式,建议使用。
保存订单信息和票据信息
在拿到票据之后,我们需要将订单信息,和票据一同传到游戏的服务器中。但是在此之前,我们还需要将订单信息和票据信息在客户端保存下来。
因为,如果请求游戏的服务器失败,那么订单和票据将不能顺利抵达游戏服务器。也就是说用户花了钱买的的道具,将不被发放。这样就造成丢单漏单,到时候每天会一大波用户到客服去投诉的。
所以,为了不必要的麻烦,我们在客户端保存一份订单信息,等到确定服务器收到以后,我们再将订单信息,从客户端删除。
获取到的票据,是一个NSData类型。我们现将票据,进行base64编码。
//base64编码
NSString *encodingReceipt = [receipt base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
然后将订单信息和票据,用url参数的格式串起来。
//获取url参数
NSString *urlParas = [NSString stringWithFormat:@"order=%@&receipt=%@" ,order ,encodingReceipt];
然后将这一串字符串,保存下来。先将这一串字符串,放到一个NSArray中,然后再用UserDefaults保存下来。
为什么要放到一个NSArray中?因为假如我们有多个订单没有发送到服务器,那么把他们都加到一个数组中,在合适的时机将他们通通拿出来,来一次统一请求,是不是很方便呢?
连接服务器
这边我们对NSURLConnection进行了简单地封装。也可以使用其他网络框架。
#pragma mark - 连接服务器
- (void)connectServer:(NSString*)urlStr urlParas:(NSString*)urlParas {
//向服务器发送验证请求
FYHttpConnection *conn = [[FYHttpConnection alloc] initWithRequest:urlStr postStr:urlParas];
[conn executeRequest:^(NSHTTPURLResponse *response, NSDictionary *data) {
SDKLog(@"---response data---%@", data);
NSNumber *code = data[@"code"];
if (code.intValue == 0) {
//交易验证成功,做交易成功处理
[self.appStoreDelegate sdkAppStorePayComplete:YES];
//从队列删除订单信息
[self removeUrlParameters:urlParas];
SDKLog(@"交易验证成功");
} else {
//交易验证失败,做交易失败处理
[self.appStoreDelegate sdkAppStorePayComplete:NO];
//从队列删除订单信息
[self removeUrlParameters:urlParas];
SDKLog(@"交易验证失败");
}
} failure:^(NSHTTPURLResponse *response, NSDictionary *data, NSError *error) {
//再请求
[self checkUnchekReceipt];
SDKLog(@"网络异常");
}];
}
当请求服务器成功,我们把订单和票据从客户端删除;当请求服务器失败的时候,我们调用[self checkUnchekReceipt]。
验证遗漏的票据
#pragma mark - 验证遗漏的票据
- (void)checkUnchekReceipt {
//取出票据
NSArray *urlParas = [self loadUrlParameters];
if ((!urlParas) || (urlParas.count == 0)) {
return;
}
for (NSString *urlPara in urlParas) {
[self connectServerForUncheckReceipt:urlPara];
}
}
我们把保存在客户端,未请求成功的订单和票据,一个个拿出来,再请求一遍。
- (void)connectServerForUncheckReceipt:(NSString*)urlPara {
//向服务器发送验证请求
FYHttpConnection *conn = [[FYHttpConnection alloc] initWithRequest:SDKAppStoreCheckUrl postStr:urlPara];
[conn executeRequest:^(NSHTTPURLResponse *response, NSDictionary *data) {
SDKLog(@"---response data---%@", data);
NSNumber *code = data[@"code"];
if (code.intValue == 0) {
//交易验证成功,做交易成功处理
//从队列删除订单信息
[self removeUrlParameters:urlPara];
SDKLog(@"交易验证成功");
} else {
//交易验证失败,做交易失败处理
//从队列删除订单信息
[self removeUrlParameters:urlPara];
SDKLog(@"交易验证失败");
}
} failure:^(NSHTTPURLResponse *response, NSDictionary *data, NSError *error) {
//再请求
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)delayTime * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self connectServerForUncheckReceipt:urlPara];
});
SDKLog(@"遗漏订单验证网络异常");
}];
}
connectServerForUncheckReceipt方法和connectServer方法没什么不同。不过在请求失败的时候,会开启一个线程,几分钟以后再请求一次,直到请求成功,再把订单和票据从客户端删掉。
为交易队列添加观察者
这样就万无一失了吗?并不是。当你的app在运行的情况下,一直没有请求成功怎么办?没关系,我们在打开app的时候,也来检查一遍,有没有遗漏的订单。
所以我们在AppDelegate的application: didFinishLaunchingWithOptions: 方法中添加[[SDKAppStore sharedInstance] checkUnchekReceipt];
有一种情况是,当用户已经把想要交易的商品,加入到交易队列里面了,而paymentQueue: updatedTransactions:却迟迟得不到响应(有时候苹果服务器响应真的很慢)。这时候用户把app关掉了(等得不耐烦了)。所以完蛋了,票据接受不到了。。
不用担心,苹果已经为我们提供了解决方案。我们在app刚打开的时候,就把交易队列的观察者加上。这样,如果系统检查到交易队列中有未完成的交易,就会去调用代码中的paymentQueue: updatedTransactions:方法,以保证我们可以收得到票据。
#pragma mark - 添加交易队列观察者
- (void)addAppStoreObserver {
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
然后我们在AppDelegate的 application:didFinishLaunchingWithOptions:方法中添加[[SDKAppStore sharedInstance] addAppStoreObserver];
总结
1.先要获取商品信息。
2.将商品加到交易队列里面。
3.根据交易队列回调,做相应的事务。
4.获取票据信息。
5.将订单信息和票据信息保存到客户端。
6.请求服务器。如果连接成功,将信息从客户端删除;连接失败,继续请求,直到请求成功。
7.在app刚打开的时候,就添加交易队列观察者。
======2016.11.02更新======
关闭交易
在保证你把订单+票据,传到自家的服务器之后,记得要关闭交易。
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
如果没有关闭交易,系统会自动判定你交易还没完成。也就是说,在你为交易队列添加观察者的情况下,每次打开app,都有可能会继续发起未完成的交易。
服务端验证
在服务端,我们把票据,传给苹果服务器去验证。
发送地址
//测试地址
https://sandbox.itunes.apple.com/verifyReceipt
//正式地址
https://buy.itunes.apple.com/verifyReceipt
发送的格式是JSON
{
"receipt-data":"你的票据"
}
返回的也是一个JSON
status表示状态码,0表示成功,其他的表示验证不通过
特别说明,21007那个状态码,表示你是在测试环境中取的票据,但是却到正式地址去验证。
利用这个,我们就不用两个地址间来回切换了。我们每次先到正式地址去验证,如果返回21007,我们就再到测试地址验证一次。
详情请看:IAP票据验证
返回的信息里面有一个receipt字段,也是JSON格式,包含了你票据的信息。
特别说明的是,receipt里面的in_app字段,这个字段包含了所有你未完成交易的票据信息。也就是在上节说到的关闭交易之后,这个票据信息,就会从in_app中消失。如果不关闭交易,这个票据信息就会在in_app中一直保留。(这个情况可能仅限于你的商品类型为消耗型)
如果你需要当前票据的唯一号,取in_app中最后一个票据的transaction_id就行。
详情请看:receipt各字段含义