iOS 蘋果的內購css
1、介紹api
蘋果規定,凡是虛擬的物品(例如:QQ音樂的樂幣)進行交易時,都必須走蘋果的內購通道,蘋果要收取大約30%的抽成,因此不容許接入第三方的支付方式(微信、支付寶等),固然開發者能夠設置後門,在審覈時避開審覈人員。這個是有風險的,一旦發現,app會被當即下架,仍是老老實實接入內購吧。數組
2、注意安全
內購接入仍是比較簡單的,蘋果提供了專門的框架<StoreKit/StoreKit.h>,只要按照它提供的api進行開發就行。然而,接入的過程仍是有須要注意的地方,分別是:漏單處理、二次驗證、移除交易、遊客模式。微信
漏單處理: 這個是必定會存在的,由於用戶的一些誤操做,形成的漏單基本沒法避免,針對這種狀況,最終的處理方式就是人工客服。固然,這個過程是能夠優化的,開發者能夠進行存儲訂單票據,server存儲訂單號,本地存儲票據。若是用戶啓動app後,檢測到用戶上次付了款,可是須要的商品沒有給到用戶,此時能夠自動進行驗證並處理,驗證經過,就將商品補給用戶。app
二次驗證:這個步驟必不可少,首先正式環境驗證,若是驗證經過,說明是線上環境,能夠正常操做。若是驗證不經過,說明是沙盒環境,須要在沙盒環境下再次驗證,沙盒環境下的驗證結果會有一個統一的彈框標識[Environment : Sandbox],只要內購沒有上線,驗證時都是沙盒環境彈框。 二次驗證的這個過程能夠避免在審覈app時,由於沒有驗證經過直接被拒的風險。二次驗證放在server端實現,更加安全。框架
移除交易:用戶再次交易時,若是上次的交易沒有被移除,那麼這次的交易會一直在隊列中等候,沒法被提交,因此必定要在上次交易完成時移除交易。ide
遊客模式:若是咱們的app支持遊客使用,那麼這個內購就必需要求對遊客進行開放,不然審覈會被拒絕。測試
3、使用fetch
一、實現代理
@interface InAppPurchaseViewController ()<SKPaymentTransactionObserver,SKProductsRequestDelegate> @property (nonatomic, strong)NSMutableArray *products; @property (nonatomic, strong)Product *currentProduct; @property (nonatomic, copy)NSString *currentPayNo; @end
二、添加觀察者
-(void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; // 添加觀察者 [[SKPaymentQueue defaultQueue] addTransactionObserver:self]; }
三、移除觀察者
-(void)viewWillDisappear:(BOOL)animated { // 移除觀察者 [[SKPaymentQueue defaultQueue] removeTransactionObserver:self]; }
四、移除沒有關閉的交易並作漏單處理
#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]; } } }
五、用戶使用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]; }]; }
六、開始內購
-(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]; } }
七、請求全部的商品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]; }
八、獲取蘋果那邊的內購監聽
//請求成功 -(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]; }
九、建立票據,在隊列中等候處理
-(void)buyProduct:(SKProduct *)product{ // 1.建立票據 SKPayment *skpayment = [SKPayment paymentWithProduct:product]; // 2.將票據加入到交易隊列 [[SKPaymentQueue defaultQueue] addPayment:skpayment]; }
十、內購回調
#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; } } }
十一、本地存儲票據
// 蘋果內購支付成功 - (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]; } }
十二、二次驗證
#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]; }]; }
1三、懶加載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; }
3、推薦
最好把監聽回調和驗證寫在單例中,這樣app一啓動時,就能夠監聽回調狀態。
4、結論
這就是內購的所有流程了,我把主要的流程梳理了一下,具體的細節,開發人員本身去整理。與君共勉。。。。。