iOS:苹果内购实践
iOS 苹果的内购
一、介绍
苹果规定,凡是虚拟的物品(例如:QQ音乐的乐币)进行交易时,都必须走苹果的内购通道,苹果要收取大约30%的抽成,所以不允许接入第三方的支付方式(微信、支付宝等),当然开发者可以设置后门,在审核时避开审核人员。这个是有风险的,一旦发现,app会被立即下架,还是老老实实接入内购吧。
二、注意
内购接入还是比较简单的,苹果提供了专门的框架<StoreKit/StoreKit.h>,只要按照它提供的api进行开发就行。然而,接入的过程还是有需要注意的地方,分别是:漏单处理、二次验证、移除交易、游客模式。
漏单处理: 这个是一定会存在的,因为用户的一些误操作,造成的漏单基本无法避免,针对这种情况,最终的处理方式就是人工客服。当然,这个过程是可以优化的,开发者可以进行存储订单票据,server存储订单号,本地存储票据。如果用户启动app后,检测到用户上次付了款,但是需要的商品没有给到用户,此时可以自动进行验证并处理,验证通过,就将商品补给用户。
二次验证:这个步骤必不可少,首先正式环境验证,如果验证通过,说明是线上环境,可以正常操作。如果验证不通过,说明是沙盒环境,需要在沙盒环境下再次验证,沙盒环境下的验证结果会有一个统一的弹框标识[Environment : Sandbox],只要内购没有上线,验证时都是沙盒环境弹框。 二次验证的这个过程可以避免在审核app时,因为没有验证通过直接被拒的风险。二次验证放在server端实现,更加安全。
移除交易:用户再次交易时,如果上次的交易没有被移除,那么此次的交易会一直在队列中等候,无法被提交,所以一定要在上次交易完成时移除交易。
游客模式:如果我们的app支持游客使用,那么这个内购就必须要求对游客进行开放,否则审核会被拒绝。
三、使用
1、实现代理
@interface InAppPurchaseViewController ()<SKPaymentTransactionObserver,SKProductsRequestDelegate> @property (nonatomic, strong)NSMutableArray *products; @property (nonatomic, strong)Product *currentProduct; @property (nonatomic, copy)NSString *currentPayNo; @end
2、添加观察者
-(void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; // 添加观察者 [[SKPaymentQueue defaultQueue] addTransactionObserver:self]; }
3、移除观察者
-(void)viewWillDisappear:(BOOL)animated { // 移除观察者 [[SKPaymentQueue defaultQueue] removeTransactionObserver:self]; }
4、移除没有关闭的交易并做漏单处理
#pragma mark 先检查之前是否有未关闭的交易并做漏单处理 -(void)checkNotCloseAndFinishedTransaction{ NSArray* transactions = [SKPaymentQueue defaultQueue].transactions; for (SKPaymentTransaction* transaction in transactions) { if (transaction.transactionState == SKPaymentTransactionStatePurchased) { [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; } } NSArray *tickets = [InAppPurchaseTicketService fetchInAppPurchaseTicket]; if (tickets.count > 0) { [MBProgressHUD showMessage:@"正在验证未处理的订单,请稍后"]; self.conchChargeView.userInteractionEnabled = NO; for (InAppPurchaseTicket *ticket in tickets) { [self checkAppStorePayResultWithTikect:ticket]; } } }
5、用户使用productId进行下单
-(void)loadPayNoData{ AppWeak(weakSelf, self); NSMutableDictionary *params = [NSMutableDictionary dictionary]; params[@"productId"] = self.currentProduct.productId; [InAppPurchaseTicketService getIosOrderWithParams:params success:^(NSArray *items, BOOL isLocalData) { if (items.count >0 ) { InAppPurchaseInfo *inAppPurchaseInfo = items[0]; weakSelf.currentPayNo = inAppPurchaseInfo.payNo; [weakSelf startPayForProduct:weakSelf.currentProduct.productId]; } } failure:^(id errorInfo) { [self showErrorInfo:errorInfo]; }]; }
6、开始内购
-(void)startPayForProduct:(NSString *)productID{ if([SKPaymentQueue canMakePayments]){ [MBProgressHUD showMessage:@"正在请求商品信息,请稍等..."]; self.conchChargeView.userInteractionEnabled = NO; // productID就是你在创建购买项目时所填写的产品ID [self requestProductID:productID]; }else{ // NSLog(@"不允许程序内付费"); UIAlertView *alertError = [[UIAlertView alloc] initWithTitle:@"温馨提示" message:@"请先开启应用内付费购买功能。" delegate:nil cancelButtonTitle:@"确定" otherButtonTitles: nil]; [alertError show]; } }
7、请求所有的商品ID
-(void)requestProductID:(NSString *)productID{ // 1.拿到所有可卖商品的ID数组 NSMutableArray *productIDArray = [NSMutableArray array]; for (Product *product in self.products) { [productIDArray addObject:product.productId]; } NSSet *sets = [[NSSet alloc] initWithArray:productIDArray]; // 2.向苹果发送请求,请求所有可买的商品 // 2.1.创建请求对象 SKProductsRequest *sKProductsRequest = [[SKProductsRequest alloc]initWithProductIdentifiers:sets]; // 2.2.设置代理(在代理方法里面获取所有的可卖的商品) sKProductsRequest.delegate = self; // 2.3.开始请求 [sKProductsRequest start]; }
8、获取苹果那边的内购监听
//请求成功 -(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{ NSArray *product = response.products; if([product count] == 0){ [MBProgressHUD hideHUD]; self.conchChargeView.userInteractionEnabled = YES; UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"提示" message:@"没有商品" delegate:nil cancelButtonTitle:@"关闭" otherButtonTitles:nil, nil]; [alertView show]; return; } for (SKProduct *sKProduct in product) { NSLog(@"SKProduct 描述信息:%@", sKProduct.description); NSLog(@"localizedTitle 产品标题:%@", sKProduct.localizedTitle); NSLog(@"localizedDescription 产品描述信息:%@",sKProduct.localizedDescription); NSLog(@"price 价格:%@",sKProduct.price); NSLog(@"productIdentifier Product id:%@",sKProduct.productIdentifier); if([sKProduct.productIdentifier isEqualToString:self.currentProduct.productId]){ [self buyProduct:sKProduct]; break; } } } //请求失败 - (void)request:(SKRequest *)request didFailWithError:(NSError *)error{ [MBProgressHUD hideHUD]; self.conchChargeView.userInteractionEnabled = YES; UIAlertView *alerView = [[UIAlertView alloc] initWithTitle:@"提示" message:[error localizedDescription] delegate:nil cancelButtonTitle:@"关闭" otherButtonTitles:nil]; [alerView show]; }
9、创建票据,在队列中等候处理
-(void)buyProduct:(SKProduct *)product{ // 1.创建票据 SKPayment *skpayment = [SKPayment paymentWithProduct:product]; // 2.将票据加入到交易队列 [[SKPaymentQueue defaultQueue] addPayment:skpayment]; }
10、内购回调
#pragma mark 4.实现观察者监听付钱的代理方法,只要交易发生变化就会走下面的方法 -(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions{ /* SKPaymentTransactionStatePurchasing, 正在购买 SKPaymentTransactionStatePurchased, 已经购买 SKPaymentTransactionStateFailed, 购买失败 SKPaymentTransactionStateRestored, 回复购买中 SKPaymentTransactionStateDeferred 交易还在队列里面,但最终状态还没有决定 */ for (SKPaymentTransaction *transaction in transactions) { switch (transaction.transactionState) { case SKPaymentTransactionStatePurchasing:{ [MBProgressHUD hideHUD]; [MBProgressHUD showMessage:@"正在购买中,别走开..."]; NSLog(@"正在购买..."); } break; case SKPaymentTransactionStatePurchased:{ // 购买后告诉交易队列,把这个成功的交易移除掉 [queue finishTransaction:transaction]; [MBProgressHUD hideHUD]; [self SavePaymentTransactionpAfterbuyAppleStoreProductSucceed:transaction]; NSLog(@"购买成功"); } break; case SKPaymentTransactionStateFailed:{ // 购买失败也要把这个交易移除掉 [queue finishTransaction:transaction]; [MBProgressHUD hideHUD]; self.conchChargeView.userInteractionEnabled = YES; NSString *errorInfo = @"购买失败,请稍后重新购买"; if (transaction.error) { NSString *reason = transaction.error.userInfo[NSLocalizedFailureReasonErrorKey]; if ([StringUtility isStringNotEmptyOrNil:reason]) { errorInfo = reason; } } UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"提示" message:errorInfo delegate:nil cancelButtonTitle:@"关闭" otherButtonTitles:nil, nil]; [alertView show]; NSLog(@"购买失败"); } break; case SKPaymentTransactionStateRestored:{ // 回复购买中也要把这个交易移除掉 [queue finishTransaction:transaction]; [MBProgressHUD hideHUD]; self.conchChargeView.userInteractionEnabled = YES; UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"提示" message:@"重复购买了" delegate:nil cancelButtonTitle:@"关闭" otherButtonTitles:nil, nil]; [alertView show]; NSLog(@"重复购买了"); } break; case SKPaymentTransactionStateDeferred:{ NSLog(@"交易还在队列里面,但最终状态还没有决定"); } break; default: break; } } }
11、本地存储票据
// 苹果内购支付成功 - (void)SavePaymentTransactionpAfterbuyAppleStoreProductSucceed:(SKPaymentTransaction *)paymentTransactionp { // 传输的是BASE64编码的字符串 // 验证凭据,获取到苹果返回的交易凭据 // appStoreReceiptURL iOS7.0增加的,购买交易完成后,会将凭据存放在该地址 NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; // 从沙盒中获取到购买凭据 NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL]; // 传输的是BASE64编码的字符串 NSString *reciept = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed]; // productIdentifier NSString *productIdentifier = paymentTransactionp.payment.productIdentifier; if ([productIdentifier length]>0) { //本地存储票据 InAppPurchaseTicket *ticket = [[InAppPurchaseTicket alloc] init]; ticket.payNo = self.currentPayNo; ticket.productId = self.currentProduct.productId; ticket.reciept = reciept; ticket.state = 1; [InAppPurchaseTicketService saveLocalTransaction:ticket]; // 去验证是否真正的支付成功了 [MBProgressHUD showMessage:@"购买成功,正在验证订单..."]; [self checkAppStorePayResultWithTikect:ticket]; } }
12、二次验证
#pragma mark 服务端验证购买凭据 - (void)checkAppStorePayResultWithTikect:(InAppPurchaseTicket *)tikect { /* 生成订单参数,注意沙盒测试账号与线上正式苹果账号的验证途径不一样,要给后台标明 注意: 自己测试的时候使用的是沙盒购买(测试环境) App Store审核的时候也使用的是沙盒购买(测试环境) 上线以后就不是用的沙盒购买了(正式环境) 所以此时应该先验证正式环境,在验证测试环境 正式环境验证成功,说明是线上用户在使用 正式环境验证不成功返回21007,说明是自己测试或者审核人员在测试 苹果AppStore线上的购买凭证地址是: https://buy.itunes.apple.com/verifyReceipt 测试地址是:https://sandbox.itunes.apple.com/verifyReceipt */ NSString *sandbox; #ifdef TEST sandbox = @"0"; //沙盒测试环境 #else sandbox = @"1"; //线上正式环境 #endif if (!tikect || !tikect.payNo || tikect.payNo.length==0 || !tikect.reciept || tikect.reciept.length==0) { return; } AppWeak(weakSelf, self); NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; params[@"payno"] = tikect.payNo; //订单号 params[@"sandbox"] = sandbox; //使用环境 params[@"receipt"] = tikect.reciept; //票据信息 [InAppPurchaseTicketService doubleIosVerifyWithParams:params success:^(NSArray *items, BOOL isLocalData) { //隐藏loding [MBProgressHUD hideHUD]; [MBProgressHUD showSuccess:@"恭喜你,购买成功" afterDelay:2.0]; weakSelf.conchChargeView.userInteractionEnabled = YES; //清除本地当前对应订单票据 InAppPurchaseInfo *info = items[0]; [InAppPurchaseTicketService clearInAppPurchaseTicketWithPayNo:info.payNo]; //刷新UI if (self.fromVCType == FromVCTypeCurrentInAppPurchaseVC) { [weakSelf loadConchData]; } else{ //回调并跳转页面 if (weakSelf.chargeConchSuccsssBlock) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ weakSelf.chargeConchSuccsssBlock(@(YES)); [weakSelf.navigationController popViewControllerAnimated:YES]; }); } } } failure:^(id errorInfo) { [MBProgressHUD hideHUD]; weakSelf.conchChargeView.userInteractionEnabled = YES; UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"验证失败" message:errorInfo delegate:nil cancelButtonTitle:@"关闭" otherButtonTitles:nil, nil]; [alertView show]; }]; }
13、懒加载plist中的所有约定的商品productId
-(NSMutableArray *)products{ if (!_products) { _products = [NSMutableArray array]; NSString *path = [[NSBundle mainBundle] pathForResource:@"Products" ofType:@"plist"]; NSArray *items = [NSArray arrayWithContentsOfFile:path]; if ([ArrayUtility isArrayNotEmptyOrNil:items]) { for (NSDictionary *dic in items) { Product *product = [[Product alloc] initWithProperties:dic]; [_products addObject:product]; } } } return _products; }
三、推荐
最好把监听回调和验证写在单例中,这样app一启动时,就可以监听回调状态。
四、结论
这就是内购的全部流程了,我把主要的流程梳理了一下,具体的细节,开发人员自己去整理。与君共勉。。。。。