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 View Code2、添加觀察者
-(void)viewWillAppear:(BOOL)animated {[super viewWillAppear:animated];// 添加觀察者 [[SKPaymentQueue defaultQueue] addTransactionObserver:self]; } View Code3、移除觀察者
-(void)viewWillDisappear:(BOOL)animated {// 移除觀察者 [[SKPaymentQueue defaultQueue] removeTransactionObserver:self]; } View Code4、移除沒有關閉的交易并做漏單處理
#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];}} } View Code5、用戶使用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];}]; } View Code6、開始內購
-(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:nilcancelButtonTitle:@"確定"otherButtonTitles: nil];[alertError show];} } View Code7、請求所有的商品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]; } View Code8、獲取蘋果那邊的內購監聽
//請求成功 -(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];} View Code9、創建票據,在隊列中等候處理
-(void)buyProduct:(SKProduct *)product{// 1.創建票據SKPayment *skpayment = [SKPayment paymentWithProduct:product];// 2.將票據加入到交易隊列 [[SKPaymentQueue defaultQueue] addPayment:skpayment]; } View Code10、內購回調
#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;}} } View Code11、本地存儲票據
// 蘋果內購支付成功 - (void)SavePaymentTransactionpAfterbuyAppleStoreProductSucceed:(SKPaymentTransaction *)paymentTransactionp {// 傳輸的是BASE64編碼的字符串// 驗證憑據,獲取到蘋果返回的交易憑據// appStoreReceiptURL iOS7.0增加的,購買交易完成后,會將憑據存放在該地址NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];// 從沙盒中獲取到購買憑據NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];// 傳輸的是BASE64編碼的字符串NSString *reciept = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];// productIdentifierNSString *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];} } View Code12、二次驗證
#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 TESTsandbox = @"0"; //沙盒測試環境 #elsesandbox = @"1"; //線上正式環境 #endifif (!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];//刷新UIif (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];}]; } View Code13、懶加載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; } View Code?
三、推薦
最好把監聽回調和驗證寫在單例中,這樣app一啟動時,就可以監聽回調狀態。
?
四、結論
這就是內購的全部流程了,我把主要的流程梳理了一下,具體的細節,開發人員自己去整理。與君共勉。。。。。
?
總結
以上是生活随笔為你收集整理的iOS:苹果内购实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 美国国家航空航天局宣布发现地球2.0
- 下一篇: CSS图片格式