(转)iOS内购(iap)总结

刚刚做了内购, 记录一下
这里直接上代码, 至于写代码之前的一些设置工作参考以下文章:
http://www.jianshu.com/p/690a7c68664e
http://www.jianshu.com/p/86ac7d3b593a

需要注意的是:

  1. 只要工程配置了对应的证书, 就能请求商品信息, 不需要任何其他处理
  2. 沙盒测试填写的邮箱不能是已经绑定appleID的邮箱, 也不能是AppleID的救援邮箱, 其他的无所谓, 其实, 哪怕你填写的邮箱不存在也没有关系

// // IAPManager.m // SpeakEnglish // // Created by Daniel on 16/6/8. // Copyright © 2016年 Daniel. All rights reserved. // #import "IAPManager.h" #import <StoreKit/StoreKit.h> @interface IAPManager ()<SKPaymentTransactionObserver, SKProductsRequestDelegate> // 所有商品 @property (nonatomic, strong)NSArray *products; @property (nonatomic, strong)SKProductsRequest *request; @end static IAPManager *manager = nil; @implementation IAPManager + (instancetype)shareIAPManager { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ manager = [self new]; [[SKPaymentQueue defaultQueue] addTransactionObserver:manager]; }); return manager; } - (void)dealloc { [[SKPaymentQueue defaultQueue] removeTransactionObserver:self]; } // 请求可卖的商品 - (void)requestProducts { if (![SKPaymentQueue canMakePayments]) { // 您的手机没有打开程序内付费购买 return; } // 1.请求所有的商品ID NSString *productFilePath = [[NSBundle mainBundle] pathForResource:@"iapdemo.plist" ofType:nil]; NSArray *products = [NSArray arrayWithContentsOfFile:productFilePath]; // 2.获取所有的productid NSArray *productIds = [products valueForKeyPath:@"productId"]; // 3.获取productid的set(集合中) NSSet *set = [NSSet setWithArray:productIds]; // 4.向苹果发送请求,请求可卖商品 _request = [[SKProductsRequest alloc] initWithProductIdentifiers:set]; _request.delegate = self; [_request start]; } /** * 当请求到可卖商品的结果会执行该方法 * * @param response response中存储了可卖商品的结果 */ - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { for (SKProduct *product in response.products) { // 用来保存价格 NSMutableDictionary *priceDic = @{}.mutableCopy; // 货币单位 NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init]; [numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4]; [numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle]; [numberFormatter setLocale:product.priceLocale]; // 带有货币单位的价格 NSString *formattedPrice = [numberFormatter stringFromNumber:product.price]; [priceDic setObject:formattedPrice forKey:product.productIdentifier]; NSLog(@"价格:%@", product.price); NSLog(@"标题:%@", product.localizedTitle); NSLog(@"秒速:%@", product.localizedDescription); NSLog(@"productid:%@", product.productIdentifier); } // 保存价格列表 [[NSUserDefaults standardUserDefaults] setObject:priceDic forKey:@"priceDic"]; [[NSUserDefaults standardUserDefaults] synchronize]; // 1.存储所有的数据 self.products = response.products; self.products = [self.products sortedArrayWithOptions:NSSortConcurrent usingComparator:^NSComparisonResult(SKProduct *obj1, SKProduct *obj2) { return [obj1.price compare:obj2.price]; }]; } #pragma mark - 购买商品 - (void)buyProduct:(SKProduct *)product { // 1.创建票据 SKPayment *payment = [SKPayment paymentWithProduct:product]; WELog(@"productIdentifier----%@", payment.productIdentifier); // 2.将票据加入到交易队列中 [[SKPaymentQueue defaultQueue] addPayment:payment]; } #pragma mark - 实现观察者回调的方法 /** * 当交易队列中的交易状态发生改变的时候会执行该方法 * * @param transactions 数组中存放了所有的交易 */ - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { /* SKPaymentTransactionStatePurchasing, 正在购买 SKPaymentTransactionStatePurchased, 购买完成(销毁交易) SKPaymentTransactionStateFailed, 购买失败(销毁交易) SKPaymentTransactionStateRestored, 恢复购买(销毁交易) SKPaymentTransactionStateDeferred 最终状态未确定 */ for (SKPaymentTransaction *transaction in transactions) { switch (transaction.transactionState) { case SKPaymentTransactionStatePurchasing: WELog(@"用户正在购买"); break; case SKPaymentTransactionStatePurchased: WELog(@"productIdentifier----->%@", transaction.payment.productIdentifier); [self buySuccessWithPaymentQueue:queue Transaction:transaction]; break; case SKPaymentTransactionStateFailed: NSLog(@"购买失败"); [queue finishTransaction:transaction]; break; case SKPaymentTransactionStateRestored: NSLog(@"恢复购买"); [queue finishTransaction:transaction]; break; case SKPaymentTransactionStateDeferred: NSLog(@"最终状态未确定"); break; default: break; } } } - (void)buySuccessWithPaymentQueue:(SKPaymentQueue *)queue Transaction:(SKPaymentTransaction *)transaction { AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; NSDictionary *params = @{@"user_id":@"user_id", // 获取商品 @"goods":[self goodsWithProductIdentifier:transaction.payment.productIdentifier]}; [manager POST:@"url" parameters:params success:^(NSURLSessionDataTask *task, id responseObject) { if ([responseObject[@"code"] intValue] == 200) { // 防止丢单, 必须在服务器确定后从交易队列删除交易 // 如果不从交易队列上删除交易, 下次调用addTransactionObserver:, 仍然会回调‘updatedTransactions‘方法, 以此处理丢单 WELog(@"购买成功"); [queue finishTransaction:transaction]; } } failure:^(NSURLSessionDataTask *task, NSError *error) { }]; } // 商品列表 也可以使用从苹果请求的数据, 具体细节自己视情况处理 // goods1 是商品的ID - (NSString *)goodsWithProductIdentifier:(NSString *)productIdentifier { NSDictionary *goodsDic = [[NSUserDefaults standardUserDefaults] objectForKey:@"priceDic"]; return goodsDic[productIdentifier]; } // 恢复购买 - (void)restore { [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; } - (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error { // 恢复失败 WELog(@"恢复失败"); } // 取消请求商品信息 - (void)dealloc { [_request cancel]; } @end 

对于丢单的处理, 这里利用苹果自带的机制, 即如果不调用‘finishTransaction‘方法, 下次调用[[SKPaymentQueue defaultQueue] addTransactionObserver:self]后会再次回调‘- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions‘方法, 所以在向自己的服务器确认了交易成功后再调用‘finishTransaction‘方法.
这样做一定程度上可以解决丢单的状况, 但是好像还有问题, 很多人都觉得应该做本地化才能更好地防止丢单, 常见的方法是:

  1. 创建票据时使用SKMutablePayment并设置applicationUsername参数, 方便后台区分用户, 给用户发商品
  2. 在‘- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions‘ 方法中, 当transactionState == SKPaymentTransactionStateRestored时保存票据到本地.
    获取票据的方式有两种。一种是直接获取SKPaymentTransaction里面的属性transactionReceipt。这种方式,在iOS7已经废弃了,到iOS9停用。但是为了兼容旧机型,最好还是加上这个方式。
  3. 每次用户登录时, 发送用户未验证的票据到服务器, 然后服务器再到AppStore验证, 根据AppStore返回的结果处理交易, 客户端视情况删除保存的票据

思考:
(1)如果考虑到用户更换手机的情况, 还是传到服务器比较安全…但是如果能够将数据上传到服务器, 那么同样也应该可以告诉服务器交易成功并请求发放商品…感觉整个人都不好了.
(2)我感觉在transactionState == SKPaymentTransactionStateRestored才上传数据最好, 假如你使用你朋友的手机购买商品并且没有马上交易成功并且退出了应用, 交易成功的回调是在
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];之后才会回调, 你很难保证你朋友在什么时候才会再次打开这个应用, 如果他把这个应用删了, 就会造成丢单. 但可惜的是, 只有在transactionState == SKPaymentTransactionStateRestored才会有transactionReceipt, 就当我什么都没说好了…
(3)仔细思考了下, 发现除了第一步之外, 其他步骤并没有起到太多的作用, 而且不本地化也可以使用. 这样看来, 好像并没有做本地化的必要, 突然感受到了来自这个世界的恶意. 我倒是希望是我逻辑上有漏洞, 如果有谁发现了请告诉我, 先行谢过.

总结:
内购有三个可能出现的问题

  1. 支付成功后, 没来得及向服务器发送交易成功的数据就退出应用, 导致丢单. 这个问题貌似不需要本地化数据也已经没问题了, 除非再次回调updatedTransactions方法时已经拿不到票据了, 这样才有必要本地存储票据.
  2. 无法绑定交易和对应的用户. 因为applicationUsername的存在这已经不是问题了.
  3. 只用别人的手机进行购买, 没来得及向服务器发送交易成功的数据就退出应用, 导致丢单. 如果别人再也不打开这个应用甚至删掉了, 目前看来, 没有办法解决

参考资料:

  1. 苹果内购二次验证 PHP代码
    http://my.oschina.net/qianglong/blog/503861

  2. In-App Purchase Programming Guide
    https://developer.apple.com/library/prerelease/content/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Introduction.html#//apple_ref/doc/uid/TP40008267

  3. iPhone In App Purchase购买完成时验证transactionReceipt
    http://www.cnblogs.com/eagley/archive/2011/06/15/2081577.html

原文地址:https://www.jianshu.com/p/8c958e75f98f