iOS 內購詳解及遇到的坑

前言

本文主要集中於代碼實現,關於建立商品網上已經有不少了,就不說了,比較簡單。git

以前作過 消耗性 和 非續訂型 內購,代碼裏一直都是這兩種。最近有個新需求,須要續訂型 VIP。如今項目裏有三種內購類型的產品了,嗯...github

 

流程與代碼

流程圖服務器

 

下面這句代碼應該在程序入口寫,這樣寫的好處是,若是有未完成的payment,進入程序後會繼續走下去。而若是是在特定頁面寫,只有進入到這個頁面纔會繼續走內購流程。app

    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];ide

 

按照流程圖優化

第一步:請求產品列表atom

是在進入內購頁面後,去咱們服務器請求spa

   [self requestProductInfo];代理

 

第二步:服務器返回 產品ID 列表rest

  成功拿到一系列產品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

相關文章
相關標籤/搜索