iOS 内购详解及遇到的坑
前言
本文主要集中于代码实现,关于创建商品网上已经有很多了,就不说了,比较简单。
之前做过 消耗性 和 非续订型 内购,代码里一直都是这两种。最近有个新需求,需要续订型 VIP。现在项目里有三种内购类型的产品了,嗯...
流程与代码
流程图
下面这句代码应该在程序入口写,这样写的好处是,如果有未完成的payment,进入程序后会继续走下去。而如果是在特定页面写,只有进入到这个页面才会继续走内购流程。
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
按照流程图
第一步:请求产品列表
是在进入内购页面后,去我们服务器请求
[self requestProductInfo];
第二步:服务器返回 产品ID 列表
成功拿到一系列产品ID
第三步:去苹果后台请求详细的产品信息(也可以使用我们服务器的信息,不去请求苹果上的信息)
[[IAPManager getInstance] requestProductsInfo:array]; //请求商品信息的代码 - (void)requestProductsInfo:(NSArray*)prodIds { SKProductsRequest* productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:prodIds]]; productsRequest.delegate = self; [productsRequest start]; }
第四步:苹果后台返回详细的产品信息
//在代理方法中,返回详细的商品信息 #pragma mark - SKProductsRequestDelegate - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { if (self.delegate && [self.delegate respondsToSelector:@selector(didReceiveProductsResponse:)]) [self.delegate didReceiveProductsResponse:response.products]; }
第五步:展示产品
- (void)didReceiveProductsResponse:(NSArray *)array { if (array != nil && array.count > 0) { //可以先按价格排个序 NSSortDescriptor* sortDes = [[NSSortDescriptor alloc] initWithKey:@"price.doubleValue" ascending:YES]; NSArray *sortArray = [array sortedArrayUsingDescriptors:[NSArray arrayWithObject:sortDes]]; _dataArray = [sortArray mutableCopy]; [tableview reloadData]; } }
这里有一个产品价格本地化
使用 NSNumberFormatter *_numberFormatter;
_numberFormatter = [[NSNumberFormatter alloc] init]; [_numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4]; [_numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle]; - (void)updatePrice { [_numberFormatter setLocale:product.priceLocale]; NSString *priceStr = [_numberFormatter stringFromNumber:price]; //priceStr 就是正确的当地价格(例如 Rs290,¥10 等) }
第六步:用户点击购买
点击某一产品购买时,应先判断
[SKPaymentQueue canMakePayments] 如果是 YES,继续
第六点五步:这里我们的流程略有差异,我们先去自己服务器下单,拿到一个订单号
第七步:发送 payment 请求
下单后,这里把订单号设置进来;苹果会透传过来,确认订单时,需要使用到订单号。
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product]; payment.quantity = count; payment.applicationUsername = orderId; [[SKPaymentQueue defaultQueue] addPayment:payment];
这时,会有弹框出现,需要用户输入账户密码购买产品,将从这个账户绑定的卡上扣钱,我们无需做什么。
第八步:代理方法中返回结果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { for (SKPaymentTransaction *transaction in transactions) { [self onTransactionCompleted:transaction]; } } //根据状态做相应的操作 - (void)onTransactionCompleted:(SKPaymentTransaction *)transaction { switch (transaction.transactionState) { case SKPaymentTransactionStatePurchased: [self completeTransaction:transaction]; break; case SKPaymentTransactionStateFailed: [self failedTransaction:transaction]; break; case SKPaymentTransactionStateRestored: [self restoreTransaction:transaction]; //订阅型和非消耗型的商品才有恢复状态 break; default: break; } }
第九步:成功支付的,会有收据,transaction 的状态是 SKPaymentTransactionStatePurchased
需要把必要信息发送给我们服务器
- (void)completeTransaction:(SKPaymentTransaction *)transaction { //这里就是第七步中设置进去的订单号,正常情况下,苹果会透传过来;偶尔的没有透传嘛,就是个坑了。填坑中会有解决方法。 NSString* orderId = transaction.payment.applicationUsername; NSString* transactionId = transaction.transactionIdentifier; NSString* transactionReceipt = [[NSString alloc] initWithData:transaction.transactionReceipt encoding:NSUTF8StringEncoding]; //收据 NSInteger count = transaction.payment.quantity; //把这些信息传给后台 ... //[self sendToServer:(id)data]; }
之后 APP端就等着服务器返回即可
第十四步:成功返回
服务器成功返回后,要关掉这次交易,这一步非常重要。
[self finishTransaction:transaction];
没有成功返回的,不要关掉。有可能是我们的服务器在某个时刻,没有响应,或者返回了错误,这时候交易还在队列中,下次打开APP,
[[SKPaymentQueue defaultQueue] addTransactionObserver:self]; //所以这句写在了程序入口处
之后,会重新触发代理方法:即从第八步再走一遍
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
下面开始填坑
坑一:确定订单时,没有订单号
经过线上运营一段时间,发现会出现少量的掉单情况。对于这类情况,能提供收据的,基本都是手动补发产品了。之后针对这类情况,优化了代码,就很少有掉单情况了。
经研究发现,我们的掉单,发生在第九步,去我们服务器确认时,苹果没有把 订单号 发过来。订单号为空,自然找不到对应的订单了。
于是,在下单成功后,先把订单保存在本地。去确认订单时,如果没有订单号,就从本地拿一下,再去确认;确认成功后,删除对应订单号。
- (void)purchaseProduct:(SKProduct*)product count:(int)count order:(NSString*)orderId
{
{
// 暂存最后一次支付订单的数据
[self saveDataWithProductIdentifier:product.productIdentifier orderId:orderId];
}
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
payment.quantity = count;
payment.applicationUsername = orderId;
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
- (void)completeTransaction:(SKPaymentTransaction *)transaction
{
NSString* orderId = transaction.payment.applicationUsername;
if (orderId == nil) {
//如果没有订单号,本地取一下订单号
orderId = [self getOrderIdWithProductIdentifier:transaction.payment.productIdentifier];
}
BOOL bVIP = [self isVipTransaction:transaction];
NSString* transactionId = transaction.transactionIdentifier;
NSString* transactionReceipt = [[NSString alloc] initWithData:transaction.transactionReceipt encoding:NSUTF8StringEncoding];
NSInteger count = transaction.payment.quantity;
...
}
- (void)finishTransaction:(SKPaymentTransaction *)transaction
{
//移除订单号
[self removeOrderIdWithProductIdentifier:transaction.payment.productIdentifier];
// remove the transaction from the payment queue.
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
//对应产品ID,保存订单号 - (void)saveDataWithProductIdentifier:(NSString *)identifier orderId:(NSString *)orderId { if (!identifier || !orderId) { return; } NSArray *pathArray = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *path = [pathArray objectAtIndex:0]; NSString *filePath = [path stringByAppendingPathComponent:Last_Product_Order_Path]; NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithContentsOfFile:filePath]; if (!dic) { dic = [NSMutableDictionary dictionary]; } [dic setValue:orderId forKey:identifier]; BOOL flag = [dic writeToFile:filePath atomically:YES]; if(!flag) { NSLog(@"orderId保存失败"); } } //获取某一订单号 - (NSString *)getOrderIdWithProductIdentifier:(NSString *)productIdentifier { if (!productIdentifier) { return nil; } NSArray *pathArray = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *path = [pathArray objectAtIndex:0]; NSString *filePath = [path stringByAppendingPathComponent:Last_Product_Order_Path]; NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithContentsOfFile:filePath]; return [dic valueForKey:productIdentifier]; } //成功后删除对应订单号 - (void)removeOrderIdWithProductIdentifier:(NSString *)productIdentifier { if (!productIdentifier) { return; } NSArray *pathArray = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *path = [pathArray objectAtIndex:0]; NSString *filePath = [path stringByAppendingPathComponent:Last_Product_Order_Path]; NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithContentsOfFile:filePath]; [dic removeObjectForKey:productIdentifier]; BOOL flag = [dic writeToFile:filePath atomically:YES]; if(!flag) { NSLog(@"orderId重新保存失败"); } }
其他多为业务逻辑,欢迎各位留言交流
代码地址:https://github.com/lionwhitcher/InAppPurchase