前言
本文主要集中於代碼實現,關於創建商品網上已經有很多了,就不說了,比較簡單。
之前做過 消耗性 和 非續訂型 內購,代碼里一直都是這兩種。最近有個新需求,需要續訂型 VIP。現在項目里有三種內購類型的產品了,嗯...
流程與代碼
流程圖
下面這句代碼應該在程序入口寫,這樣寫的好處是,如果有未完成的payment,進入程序后會繼續走下去。而如果是在特定頁面寫,只有進入到這個頁面才會繼續走內購流程。
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
按照流程圖
第一步:請求產品列表
是在進入內購頁面后,去我們服務器請求
[self requestProductInfo];
第二步:服務器返回 產品ID 列表
成功拿到一系列產品ID
第三步:去蘋果后台請求詳細的產品信息(也可以使用我們服務器的信息,不去請求蘋果上的信息)
[[IAPManager getInstance] requestProductsInfo:array]; //請求商品信息的代碼 - (void)requestProductsInfo:(NSArray*)prodIds { SKProductsRequest* productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:prodIds]]; productsRequest.delegate = self; [productsRequest start]; }
第四步:蘋果后台返回詳細的產品信息
//在代理方法中,返回詳細的商品信息 #pragma mark - SKProductsRequestDelegate - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response { if (self.delegate && [self.delegate respondsToSelector:@selector(didReceiveProductsResponse:)]) [self.delegate didReceiveProductsResponse:response.products]; }
第五步:展示產品
- (void)didReceiveProductsResponse:(NSArray *)array { if (array != nil && array.count > 0) { //可以先按價格排個序 NSSortDescriptor* sortDes = [[NSSortDescriptor alloc] initWithKey:@"price.doubleValue" ascending:YES]; NSArray *sortArray = [array sortedArrayUsingDescriptors:[NSArray arrayWithObject:sortDes]]; _dataArray = [sortArray mutableCopy]; [tableview reloadData]; } }
這里有一個產品價格本地化
使用 NSNumberFormatter *_numberFormatter;
_numberFormatter = [[NSNumberFormatter alloc] init]; [_numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4]; [_numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle]; - (void)updatePrice { [_numberFormatter setLocale:product.priceLocale]; NSString *priceStr = [_numberFormatter stringFromNumber:price]; //priceStr 就是正確的當地價格(例如 Rs290,¥10 等) }
第六步:用戶點擊購買
點擊某一產品購買時,應先判斷
[SKPaymentQueue canMakePayments] 如果是 YES,繼續
第六點五步:這里我們的流程略有差異,我們先去自己服務器下單,拿到一個訂單號
第七步:發送 payment 請求
下單后,這里把訂單號設置進來;蘋果會透傳過來,確認訂單時,需要使用到訂單號。
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product]; payment.quantity = count; payment.applicationUsername = orderId; [[SKPaymentQueue defaultQueue] addPayment:payment];
這時,會有彈框出現,需要用戶輸入賬戶密碼購買產品,將從這個賬戶綁定的卡上扣錢,我們無需做什么。
第八步:代理方法中返回結果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { for (SKPaymentTransaction *transaction in transactions) { [self onTransactionCompleted:transaction]; } } //根據狀態做相應的操作 - (void)onTransactionCompleted:(SKPaymentTransaction *)transaction { switch (transaction.transactionState) { case SKPaymentTransactionStatePurchased: [self completeTransaction:transaction]; break; case SKPaymentTransactionStateFailed: [self failedTransaction:transaction]; break; case SKPaymentTransactionStateRestored: [self restoreTransaction:transaction]; //訂閱型和非消耗型的商品才有恢復狀態 break; default: break; } }
第九步:成功支付的,會有收據,transaction 的狀態是 SKPaymentTransactionStatePurchased
需要把必要信息發送給我們服務器
- (void)completeTransaction:(SKPaymentTransaction *)transaction { //這里就是第七步中設置進去的訂單號,正常情況下,蘋果會透傳過來;偶爾的沒有透傳嘛,就是個坑了。填坑中會有解決方法。 NSString* orderId = transaction.payment.applicationUsername; NSString* transactionId = transaction.transactionIdentifier; NSString* transactionReceipt = [[NSString alloc] initWithData:transaction.transactionReceipt encoding:NSUTF8StringEncoding]; //收據 NSInteger count = transaction.payment.quantity; //把這些信息傳給后台 ... //[self sendToServer:(id)data]; }
之后 APP端就等着服務器返回即可
第十四步:成功返回
服務器成功返回后,要關掉這次交易,這一步非常重要。
[self finishTransaction:transaction];
沒有成功返回的,不要關掉。有可能是我們的服務器在某個時刻,沒有響應,或者返回了錯誤,這時候交易還在隊列中,下次打開APP,
[[SKPaymentQueue defaultQueue] addTransactionObserver:self]; //所以這句寫在了程序入口處
之后,會重新觸發代理方法:即從第八步再走一遍
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
下面開始填坑
坑一:確定訂單時,沒有訂單號
經過線上運營一段時間,發現會出現少量的掉單情況。對於這類情況,能提供收據的,基本都是手動補發產品了。之后針對這類情況,優化了代碼,就很少有掉單情況了。
經研究發現,我們的掉單,發生在第九步,去我們服務器確認時,蘋果沒有把 訂單號 發過來。訂單號為空,自然找不到對應的訂單了。
於是,在下單成功后,先把訂單保存在本地。去確認訂單時,如果沒有訂單號,就從本地拿一下,再去確認;確認成功后,刪除對應訂單號。
- (void)purchaseProduct:(SKProduct*)product count:(int)count order:(NSString*)orderId
{
{
// 暫存最后一次支付訂單的數據
[self saveDataWithProductIdentifier:product.productIdentifier orderId:orderId];
}
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
payment.quantity = count;
payment.applicationUsername = orderId;
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
- (void)completeTransaction:(SKPaymentTransaction *)transaction
{
NSString* orderId = transaction.payment.applicationUsername;
if (orderId == nil) {
//如果沒有訂單號,本地取一下訂單號
orderId = [self getOrderIdWithProductIdentifier:transaction.payment.productIdentifier];
}
BOOL bVIP = [self isVipTransaction:transaction];
NSString* transactionId = transaction.transactionIdentifier;
NSString* transactionReceipt = [[NSString alloc] initWithData:transaction.transactionReceipt encoding:NSUTF8StringEncoding];
NSInteger count = transaction.payment.quantity;
...
}
- (void)finishTransaction:(SKPaymentTransaction *)transaction
{
//移除訂單號
[self removeOrderIdWithProductIdentifier:transaction.payment.productIdentifier];
// remove the transaction from the payment queue.
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
//對應產品ID,保存訂單號 - (void)saveDataWithProductIdentifier:(NSString *)identifier orderId:(NSString *)orderId { if (!identifier || !orderId) { return; } NSArray *pathArray = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *path = [pathArray objectAtIndex:0]; NSString *filePath = [path stringByAppendingPathComponent:Last_Product_Order_Path]; NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithContentsOfFile:filePath]; if (!dic) { dic = [NSMutableDictionary dictionary]; } [dic setValue:orderId forKey:identifier]; BOOL flag = [dic writeToFile:filePath atomically:YES]; if(!flag) { NSLog(@"orderId保存失敗"); } } //獲取某一訂單號 - (NSString *)getOrderIdWithProductIdentifier:(NSString *)productIdentifier { if (!productIdentifier) { return nil; } NSArray *pathArray = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *path = [pathArray objectAtIndex:0]; NSString *filePath = [path stringByAppendingPathComponent:Last_Product_Order_Path]; NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithContentsOfFile:filePath]; return [dic valueForKey:productIdentifier]; } //成功后刪除對應訂單號 - (void)removeOrderIdWithProductIdentifier:(NSString *)productIdentifier { if (!productIdentifier) { return; } NSArray *pathArray = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *path = [pathArray objectAtIndex:0]; NSString *filePath = [path stringByAppendingPathComponent:Last_Product_Order_Path]; NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithContentsOfFile:filePath]; [dic removeObjectForKey:productIdentifier]; BOOL flag = [dic writeToFile:filePath atomically:YES]; if(!flag) { NSLog(@"orderId重新保存失敗"); } }
其他多為業務邏輯,歡迎各位留言交流
代碼地址:https://github.com/lionwhitcher/InAppPurchase