iOS 内购相关
下面总结一下过往订阅和内购的项目的代码方面的实现细节和注意事项,特别是掉单方面的处理。
后台的协议、商品ID、银行卡、内购类型、沙盒账号测试人员都由运营或者产品在苹果后台中申请处理。
这里主要讲内购的代码,内购的代码主要分为两大部分:商品的查询、商品的购买。
1、首先先创建一个单例,创建单例的第一时间同时要加上对苹果订单状态变化的监听[[SKPaymentQueue defaultQueue] addTransactionObserver:self];这样所有的历史订单都会回调过来,包括已经订阅的、订阅中的、已经内购的、正在内购中的订单,回调在这个方法- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions,苹果会回调历史的订单过来,初始化之后添加监听会有一次机会处理掉单的问题。
+ (instancetype)shareManager { static PAIAPPurchaseManager *obj = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ obj = [[PAIAPPurchaseManager alloc] init]; }); return obj; } - (instancetype)init { self = [super init]; if (self) { _purchaseDic = [NSMutableDictionary dictionary]; [[SKPaymentQueue defaultQueue] addTransactionObserver:self]; } return self; }
1.1 苹果后台正式环境和沙盒环境的验单链接
//内购产品相关属性 static NSString *const IAP_SANDBOX_URL = @"https://sandbox.itunes.apple.com/verifyReceipt"; static NSString *const IAP_APPSTORE_URL = @"https://buy.itunes.apple.com/verifyReceipt";
2、在appdelegate中初始化查询商品详情,可以把对应的商品ID写在本地。
/* @param dict 获取本地的商品字典 */ - (void)initProductWithLocalDict:(NSDictionary *)dict { if (dict == nil) { return; } _requestType = PARequestTypeProductDetail; NSLog(@"initProductWithLocalDict------------>%@",dict); PAIAPProductModel *yearModel = [PAIAPProductModel yy_modelWithDictionary:dict[kPAPurchaseYearProductKey]]; PAIAPProductModel *monthModel = [PAIAPProductModel yy_modelWithDictionary:dict[kPAPurchaseMonthProductKey]]; PAIAPProductModel *weekModel = [PAIAPProductModel yy_modelWithDictionary:dict[kPAPurchaseWeekProductKey]]; [self.purchaseDic setValue:yearModel forKey:kPAPurchaseYearProductKey]; [self.purchaseDic setValue:monthModel forKey:kPAPurchaseMonthProductKey]; [self.purchaseDic setValue:weekModel forKey:kPAPurchaseWeekProductKey]; NSMutableArray *productIdArr = [NSMutableArray array]; [productIdArr addObject:yearModel.product_id]; [productIdArr addObject:monthModel.product_id]; [productIdArr addObject:weekModel.product_id]; [self getPurcaseProductPriceWithProductIDs:productIdArr]; } - (void)getPurcaseProductPriceWithProductIDs:(NSArray *)productIDs { NSLog(@"getPurcaseProductPriceWithProductIDs"); if (productIDs.count == 0) { return; } if ([SKPaymentQueue canMakePayments]) { NSArray *products = [NSArray arrayWithArray:productIDs]; NSSet *set = [NSSet setWithArray:products]; SKProductsRequest *paymentRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:set]; paymentRequest.delegate = self; [paymentRequest start]; } }
3、在发起商品查询的请求之后会走下面的回调方法,需要注意的是商品的购买和商品的查询都会走下面这个回调方法,所以要通过某个状态区分它是商品购买过来的回调还是商品查询过来的回调,这里不建议用枚举的形式来判断,因为如果用户手速比较快,在你初始化查询商品信息的回调还没过来,你就点击了购买之后的话,那么这两个回调你是区分不出哪个是查询商品,哪个是商品购买的。这里的解决方案是通过判读request是查询的request还是购买的request来区分。详细代码如下:
#pragma mark - ------SKProductsRequestDelegate------ - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { NSLog(@"查询商品"); NSArray *products = response.products; if (self.pruductMsgRequest == request) { [products enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { SKProduct *product = (SKProduct *)obj; for (NSInteger i=0; i<self.purchaseDic.allValues.count; i++) { PAIAPProductModel *model = self.purchaseDic.allValues[i]; if ([model.product_id isEqualToString:product.productIdentifier]) { model.currency_code = [product.priceLocale objectForKey:NSLocaleCurrencySymbol]; model.price = [product.price floatValue]; model.product_id = product.productIdentifier; NSLog(@"product_id:%@,currency_code:%@,price:%.2f",model.product_id,model.currency_code,model.price); } } }]; //把获取产品详细信息传递给委托 if (self.productUpdateBlock) { self.productUpdateBlock(); } }else if (self.purchaseRequest == request) { SKProduct *purchaseProduct; for (NSInteger i=0; i<products.count; i++) { purchaseProduct = products[i]; NSLog(@"商品信息:productId:%@,price:%@",purchaseProduct.productIdentifier,purchaseProduct.price); if ([purchaseProduct.productIdentifier isEqualToString:self.productId]) { break; } } if (purchaseProduct == nil) { NSLog(@"商品信息为空,找不到对应的商品"); if (self.purchaseFailureBlock) { self.purchaseFailureBlock(kString(@"purchase_State_GoodsEmpty")); [PAStatistics.operationCode(@"purchase_product_not_found").statisticsObject(self.productId) upload104Error]; } return; } SKPayment *payment = [SKPayment paymentWithProduct:purchaseProduct]; [[SKPaymentQueue defaultQueue] addPayment:payment]; } } - (void)request:(SKRequest *)request didFailWithError:(NSError *)error { NSLog(@"request product fail"); if (self.pruductMsgRequest == request) { }else if (self.purchaseRequest == request) { if (self.purchaseFailureBlock) { self.purchaseFailureBlock(error.localizedDescription); } } } - (void)requestDidFinish:(SKRequest *)request { }
在上面的代码中,查询到商品在不同的appleID(比如说美区的账号就显示美元)对应的价钱之后就可以通过block的形式回调刷新本地的价钱了。购买商品的回调中,查询到对应的商品之后就创建一笔订单,添加到购买的队列中,然后购买的流程就会走另外一套回调方法。
#pragma mark - ------SKPaymentTransactionObserver------ - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions { NSLog(@"paymentQueue updatedTransactions"); for (SKPaymentTransaction *paymentTransaction in transactions) { SKPaymentTransactionState paymentTransactionState = paymentTransaction.transactionState; switch (paymentTransactionState) { case SKPaymentTransactionStatePurchasing: { NSLog(@"paymentQueue SKPaymentTransactionStatePurchasing --------->"); NSArray* transactionArr = [SKPaymentQueue defaultQueue].transactions; if (transactionArr.count > 0) { // 检测是否有内购未完成的订单 for (SKPaymentTransaction* transaction in transactionArr) { if (transaction.transactionState == SKPaymentTransactionStatePurchased) { //保存在本地 //把交易凭证保存在本地 [self saveReceiptPurcaseAtSandboxWithTransaction:paymentTransaction]; [[SKPaymentQueue defaultQueue] finishTransaction:paymentTransaction]; } } } break; } case SKPaymentTransactionStatePurchased: { NSLog(@"paymentQueue SKPaymentTransactionStatePurchased --------->"); //保存在本地 //把交易凭证保存在本地 [self saveReceiptPurcaseAtSandboxWithTransaction:paymentTransaction]; [[SKPaymentQueue defaultQueue] finishTransaction:paymentTransaction]; break; } case SKPaymentTransactionStateFailed: { //购买失败 [self failPurcaseWithTransaction:paymentTransaction]; break; } case SKPaymentTransactionStateRestored: { NSLog(@"paymentQueue SKPaymentTransactionStateRestored --------->"); [self restorePurcaseWithTransaction:paymentTransaction]; break; } default: [[SKPaymentQueue defaultQueue] finishTransaction:paymentTransaction]; break; } } } - (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue { NSLog(@"paymentQueueRestoreCompletedTransactionsFinished--------->"); [self sendRestoreRequest]; } - (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error { NSLog(@"restoreCompletedTransactionsFailedWithError--------->"); if (self.restoreFailureBlock) { self.restoreFailureBlock(error.localizedDescription); } }
- (void)failPurcaseWithTransaction:(SKPaymentTransaction *)transaction { NSLog(@"failPurcaseWithTransaction error code : %ld", transaction.error.code); switch (transaction.error.code) { case SKErrorUnknown: { if (self.purchaseFailureBlock) { self.purchaseFailureBlock(transaction.error.localizedDescription); } break; } case SKErrorPaymentCancelled: { if (self.purchaseFailureBlock) { self.purchaseFailureBlock([self purcaseMsgWithPurcaseState:PAPurchaseStateUserCancel]); } break; } case SKErrorPaymentNotAllowed: { if (self.purchaseFailureBlock) { self.purchaseFailureBlock([self purcaseMsgWithPurcaseState:PAPurchaseStateNoRight]); } break; } default: { if (self.purchaseFailureBlock) { self.purchaseFailureBlock([self purcaseMsgWithPurcaseState:PAPurchaseStateBuyFailed]); } break; } } [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; }
上面在购买中之所以还要遍历[SKPaymentQueue defaultQueue].transactions;数组,是因为在购买的时候会触发这里的回调方法,在这里又提供多了一次处理掉单的机会。而且要用[SKPaymentQueue defaultQueue].transactions;这个数组,这个数组包含所有的历史订单,回调方法回调过来的数组只是包含部分订单,不一定准确。
4、向苹果后台发起验单,这个可以在客户端上验单也可以在服务器上验单。在客户端上验单的话会增加被篡改的风险,所以如果条件允许还是服务器验单会比较好,而且客户端和服务端之间定一套加密的规则,降低订单凭证被篡改的风险。下面给的是客户端本地验单的逻辑:
//保存购买记录到沙盒 - (void)saveReceiptPurcaseAtSandboxWithTransaction:(SKPaymentTransaction *)transaction { NSLog(@"saveReceiptPurcaseAtSandboxWithTransaction start ---->"); WS(ws); [self checkReceiptFromAppStoreWithURL:[NSURL URLWithString:IAP_SANDBOX_URL] success:^(NSData *data) { [ws savePurcaseDetailAtDocumentWithData:data]; } failure:^(NSError *error) { if (error.code == 21008) { __weak typeof(ws) wws = ws; [ws checkReceiptFromAppStoreWithURL:[NSURL URLWithString:IAP_APPSTORE_URL] success:^(NSData *data) { [wws savePurcaseDetailAtDocumentWithData:data]; } failure:^(NSError *error) { if (wws.purchaseFailureBlock) { wws.purchaseFailureBlock(error.localizedDescription); wws.purchaseFailureBlock = nil; } }]; } else { if (ws.purchaseFailureBlock) { ws.purchaseFailureBlock(error.localizedDescription); ws.purchaseFailureBlock = nil; } } }]; } - (void)savePurcaseDetailAtDocumentWithData:(NSData *)data { NSLog(@"savePurcaseDetailAtDocumentWithData start ---->"); NSDictionary *receipt = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil]; PAIAPReceiptModel *receiptModel = [[PAIAPReceiptModel alloc] init]; receiptModel.environment = receipt[@"environment"]; NSDictionary *receiptInfo = [self getMostValuableRecepitWithReceiptDict:receipt]; receiptModel.originalPurcaseDate = [NSDate UTCDateFormETCStr:receiptInfo[@"original_purchase_date"]]; receiptModel.purcaseDate = [NSDate UTCDateFormETCStr:receiptInfo[@"purchase_date"]]; receiptModel.expiresDate = [NSDate UTCDateFormETCStr:receiptInfo[@"expires_date"]]; receiptModel.productID = receiptInfo[@"product_id"]; receiptModel.webOrderID = receiptInfo[@"original_transaction_id"]; NSLog(@"<parseReceiptSavePurcaseDetailAtDocumentWithData> -------> productId : %@ expires_date : %@", receiptModel.productID, receiptModel.expiresDate); [PAFileManager savePurchaseReceiptData:receiptModel]; self.productId = nil; NSString *remark = [PAAppUserDataManager user104Remark]; [[PAAppUserDataManager sharedInstance] refreshUserPaymentState]; if ([PAAppUserDataManager sharedInstance].userPaymentState == PAUserPaymentStatusVIP && self.requestType == PARequestTypePurcase) { NSLog(@"上传订单追踪"); } if (self.purchaseSuccessBlock) { self.purchaseSuccessBlock(receiptModel,remark); } }
- (NSDictionary *)getMostValuableRecepitWithReceiptDict:(NSDictionary *)receipt{ NSDictionary *mostValueRecepit; for( NSDictionary *temp in receipt[@"latest_receipt_info"]) { NSDate *expiresDate = [NSDate UTCDateFormETCStr:temp[@"expires_date"]]; // NSString *productID = temp[@"product_id"]; NSLog(@"<getMostValuableRecepitWithReceiptDict> Recepit data ------> : %@", temp); if (mostValueRecepit == nil) { mostValueRecepit = temp; } else { NSDate *recepitExpiresDate = [NSDate UTCDateFormETCStr:mostValueRecepit[@"expires_date"]]; if ([expiresDate compare:recepitExpiresDate] != NSOrderedAscending) { mostValueRecepit = temp; } } } // NSLog(@"<getMostValuableRecepitWithReceiptDict> mostValueRecepit : %@", mostValueRecepit); return mostValueRecepit; }
验单成功之后保存凭证在本地,每次启动的时候通过过期日期判断用户是否为VIP用户就可以了。
5、另外还有恢复购买的逻辑,也比较简单,下面贴上代码,看看应该就明白了。
/* * 恢复订阅 */ - (void)restorePurchaseSuccess:(RestoreSuccessBlock)successBlock failure:(RestoreFailureBlock)failBlock{ NSLog(@"restorePurchase"); self.restoreSuccessBlock = successBlock; self.restoreFailureBlock = failBlock; [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; } - (void)sendRestoreRequest { NSLog(@"sendRestoreRequest"); WS(ws); [self checkReceiptFromAppStoreWithURL:[NSURL URLWithString:IAP_SANDBOX_URL] success:^(NSData *data) { [ws restoreCheckReceiptWithData:data]; } failure:^(NSError *error) { if (error.code == 21008) { __weak typeof(ws) wws = ws; [ws checkReceiptFromAppStoreWithURL:[NSURL URLWithString:IAP_APPSTORE_URL] success:^(NSData *data) { [wws restoreCheckReceiptWithData:data]; } failure:^(NSError *error) { if (error.code == 99999) { if (wws.restoreFailureBlock) { wws.restoreFailureBlock(kString(@"payment_restore_fail")); } } else { if (wws.restoreFailureBlock) { wws.restoreFailureBlock(error.localizedDescription); } } }]; } else { if (error.code == 99999) { if (ws.restoreFailureBlock) { ws.restoreFailureBlock(kString(@"payment_restore_fail")); } } else { if (ws.restoreFailureBlock) { ws.restoreFailureBlock(error.localizedDescription); } } } }]; } - (void)restoreCheckReceiptWithData:(NSData *)data { NSLog(@"restoreCheckReceiptWithData"); if (data == nil) { NSLog(@"restoreCheckReceiptWithData data is null"); if (self.restoreFailureBlock) { self.restoreFailureBlock(kString(@"payment_restore_fail")); } return; } PAIAPReceiptModel *receiptModel = [[PAIAPReceiptModel alloc] init]; NSDictionary *receipt = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil]; receiptModel.environment = receipt[@"environment"]; NSDictionary *receiptInfo = [self getMostValuableRecepitWithReceiptDict:receipt]; receiptModel.expiresDate = [NSDate UTCDateFormETCStr:receiptInfo[@"expires_date"]]; receiptModel.productID = receiptInfo[@"product_id"]; receiptModel.webOrderID = receiptInfo[@"original_transaction_id"]; [PAFileManager savePurchaseReceiptData:receiptModel]; PAUserPaymentStatus status = [self getUserPaymentStateWithProductId:receiptModel.productID expiresDate:receiptModel.expiresDate]; NSString *remark = [PAAppUserDataManager user104Remark]; [[PAAppUserDataManager sharedInstance] refreshUserPaymentState]; if (status == PAUserPaymentStatusVIP) { if (self.restoreSuccessBlock) { self.restoreSuccessBlock(receiptModel,remark); } }else { if (self.restoreFailureBlock) { self.restoreFailureBlock(kString(@"payment_restore_fail")); } } }
注意:
另外倘若一个项目中继承了内购和订阅的话,验单的时候可以在
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions 方法中判断paymentTransaction.payment.productIdentifier商品ID的归属的方法来判断是内购还是订阅。