[貝聊科技]貝聊 IAP 實戰之滿地是坑

你們好,我是貝聊科技 的 iOS 工程師 @NewPangit

注意:文章中討論的 IAP 是指使用蘋果內購購買消耗性的項目。github

此次爲你們帶來我司 IAP 的實現過程詳解,鑑於支付功能的重要性以及複雜性,文章會很長,並且支付驗證的細節也關係重大,因此這個主題會包含三篇。服務器

第一篇:[iOS]貝聊 IAP 實戰之滿地是坑,這一篇是支付基礎知識的講解,主要會詳細介紹 IAP,同時也會對比支付寶和微信支付,從而引出 IAP 的坑和注意點。微信

第二篇:[iOS]貝聊 IAP 實戰之見坑填坑,這一篇是高潮性的一篇,主要針對第一篇文章中分析出的 IAP 的問題進行具體解決。網絡

第三篇:[iOS]貝聊 IAP 實戰之訂單綁定,這一篇是關鍵性的一篇,主要講述做者探索將本身服務器生成的訂單號綁定到 IAP 上的過程。app

不用擔憂,我歷來不會只講原理不留源碼,我已經將我司的源碼整理出來,你使用時只須要拽到工程中就能夠了,下面開始咱們的內容 。ide

源碼在這裏。post

01.題外話

今年上半年的公衆號打賞事件,你們可還記得?咱們對蘋果強收過路費的行爲憤懣,也爲微信惋惜不已,此事最後以騰訊高管團隊訪問蘋果畫上句號。顯然,協商結果兩位老闆以及他們的團隊都很滿意。fetch

02.熟悉的支付寶和微信支付

仔細看一下下面這張圖,這是咱們每次在買早餐使用支付寶支付的流程圖。下面咱們來一步一步看一下每一步對應的操做原理。微信支付

第一步:咱們的 APP 發起一筆支付交易,此時,第一件事,咱們要去咱們本身的服務器上建立一個訂單信息。同時服務器會組裝好一筆交易交給咱們。關於組裝交易信息,有兩種作法,第一種就是支付寶推薦咱們作的,由咱們服務器來組裝交易信息,服務器加密交易信息,並保存簽名信息;另外一種作法是,服務器返回商品信息給 APP,由 APP 來組裝交易信息,並進行加密處理等操做。顯然咱們應該採用第一種方式。

第二步:服務器建立好交易信息之後,返回給 APP,APP 不對交易信息作處理。

第三步:APP 拿到交易信息,開始調起支付寶的 SDK,支付寶的 SDK 把交易信息傳給支付寶的服務器。

第四步:驗證經過之後,支付寶服務器會告訴支付寶 SDK 驗證經過。

第五步:驗證經過之後,咱們的 APP 會調起支付寶 APP,跳轉到支付寶 APP。

第六步:在支付寶 APP 裏,用戶輸入密碼進行交易,和支付寶服務器進行通信。

第七步:支付成功,支付寶服務器回調支付寶 APP。

第八步:支付寶回到咱們本身的 APP,並經過 - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation 方法處理支付寶的回調結果,對應的進行刷新 UI 等操做。

第九步:支付寶服務器會回調咱們的服務器並把收據傳給咱們服務器,若是咱們的服務器沒有確認已經收到支付寶的收據信息,那麼支付寶服務器就會一直回調咱們的服務器,只是回調時間間隔會愈來愈久。

第十步:咱們的服務器收到支付寶的回調,並回調支付寶,確認已經收到收據信息,此時早餐買完了。

支付寶的支付流程講完了,那微信支付也講完了,由於它們流程類似。

03.坑爹的 IAP 支付

IAP 坑爹之處從如下兩個方面來理解。

第一方面,APP 不接 IAP 審覈不讓過。接不接 IAP,蘋果不是和你商量,而是強制要求,爸爸說怎麼樣,就怎麼樣。固然,這篇文章解決不了這個問題,因此也只是說說而已。上面說了微信公衆號的事情,雖然它不是 IAP 的事情,可是實質上都屬於強收過路費的行爲。

第二方面,坑開發人員。下面開始數坑。

只有 8 步,比支付寶少 2 步,對不對?看起來比支付寶還簡單,有木有?

第一步:用戶開始購買,首先會去咱們本身的服務器建立一個交易訂單,返回給 APP。

第二步:APP 拿到交易信息,而後開始調起 IAP 服務建立訂單,並把訂單推入支付隊列。

第三步:IAP 會和 IAP 服務器通信,讓用戶確認購買,輸入密碼。

第四步:IAP 服務器回調 APP,通知購買成功,並把收據寫入到 APP 沙盒中。

第五步:此時,APP 應該去獲取沙盒中的收據信息(一段 Base 64 編碼的數據),並將收據信息上傳給服務器。

第六步:服務器拿到收據之後,就應該去 IAP 服務器查詢這個收據對應的已付款的訂單號。

第七步:咱們本身的服務器拿到這個收據對應的已付款的訂單號之後,就去校驗當前的已付款訂單中是否有要查詢的那一筆,若是有,就告訴 APP。

第八步:APP 拿到查詢結果,而後把這筆交易給 finish 掉。

04.對比支付寶和 IAP

沒啥大毛病,對吧?如今來詳細分析一下。

因爲移動端所處的網絡環境遠遠比服務端要複雜,因此,最大可能出現問題的是與移動端的通信上。對於支付寶,只要移動端確實付款完成,那麼接下來的驗證工做都是服務器於服務器之間的通信。這樣一來,只要用戶確實產生了一筆交易,那麼接下來的驗證就變得可靠的多,並且支付寶服務器會一直回調咱們的服務器,交易的可靠性獲得了極大的保證。

一樣,咱們再來看看 IAP,交易是同樣的。可是驗證交易這一環須要移動端來驅動咱們本身的服務器來進行查詢,這是第一個坑,先記一筆。另一點,IAP 的服務器遠在美國,咱們的服務器去查詢延時至關嚴重,這是其二

05.IAP 設計上的坑

上面講了兩個很大的坑,接下來看一看 IAP 自己有哪些坑。最大的一個就是,從 IAP 交易結果出來到通知 APP,只有一次。這裏有如下幾個問題:

1.若是用戶後買成功之後,網絡就不行了,那麼蘋果的 IAP 也收不到支付成功的通知,就無法通知 APP,咱們也無法給用戶發貨。

2.若是 IAP 通知咱們支付成功,咱們驅動服務器去 IAP 服務器查詢失敗的話,那就要等下次 APP 啓動的時候,纔會從新通知咱們有未驗證的訂單。這個週期根本無法想象,若是用戶一個月不重啓 APP,那麼咱們可能一個月無法給用戶發貨。

3.有人反饋,IAP 通知已經交易成功了,此時去沙盒裏取收據數據,發現爲空,或者出現通知交易成功那筆交易沒有被及時的寫入到沙盒數據中,致使咱們服務器去 IAP 服務器查詢的時候,查不到這筆訂單。

4.若是用戶的交易尚未獲得驗證,就把 APP 給卸載了,之後要怎麼恢復那些沒有被驗證的訂單?

5.越獄手機有無數奇葩的收據丟失或無效或被替換的問題,應該怎樣酌情處理?

6.交易沒有發生變化,僅僅是重啓一下,收據信息就會發生改變。

7.當驗證交易成功之後咱們去取 IAP 的待驗證交易列表的時候,這個列表沒有數據。

好吧,算起來有九個比較大的問題了,還有沒照顧到的請各位補充。這九個問題,基本上每個都是致命的。這麼多的不肯定性,咱們應該怎麼綜合處理,怎麼相互平衡?

咱們先放一放這些問題,下一篇就一塊兒來着手解決這些問題,如今咱們先來看一看 IAP 支付的基本代碼。

06.IAP 支付代碼

咱們先不去想那麼多,先把支付邏輯跑通再說。下面咱們看看 IAP 的代碼。

#import <StoreKit/StoreKit.h>

@interface BLPaymentManager ()<SKPaymentTransactionObserver, SKProductsRequestDelegate>

@end

@implementation BLPaymentManager

- (void)dealloc {
    [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}

- (void)init {
    self = [super init];
    if(self) {
         [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    }
    return self;
}

- (void)buyProduction {
    if ([SKPaymentQueue canMakePayments]) {
        
        [self getProductInfo:nil];
        
    } else {
        NSLog(@"用戶禁止應用內付費購買");
    }
}

// 從Apple查詢用戶點擊購買的產品的信息.
- (void)getProductInfo:(NSString *)productIdentifier {
    NSSet *identifiers = [NSSet setWithObject:productIdentifier];
    SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:identifiers];
    request.delegate = self;
    [request start];
}


#pragma mark - SKPaymentTransactionObserver

// 購買操做後的回調.
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
    // 這裏的事務包含以前沒有完成的.
    for (SKPaymentTransaction *transcation in transactions) {
        switch (transcation.transactionState) {
            case SKPaymentTransactionStatePurchasing:
                [self transcationPurchasing:transcation];
                break;
                
            case SKPaymentTransactionStatePurchased:
                [self transcationPurchased:transcation];
                break;
                
            case SKPaymentTransactionStateFailed:
                [self transcationFailed:transcation];
                break;
                
            case SKPaymentTransactionStateRestored:
                [self transcationRestored:transcation];
                break;
                
            case SKPaymentTransactionStateDeferred:
                [self transcationDeferred:transcation];
                break;
        }
    }
}


#pragma mark - TranscationState

// 交易中.
- (void)transcationPurchasing:(SKPaymentTransaction *)transcation {
    NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
    if (!receipt) {
        NSLog(@"沒有收據, 處理異常");
        return;
    }
}

// 交易成功.
- (void)transcationPurchased:(SKPaymentTransaction *)transcation {
    // 存儲到本地先.
    // 發送到服務器, 等待驗證結果.
    [[SKPaymentQueue defaultQueue] finishTransaction:transcation];
}

// 交易失敗.
- (void)transcationFailed:(SKPaymentTransaction *)transcation {
    
}

// 已經購買過該商品.
- (void)transcationRestored:(SKPaymentTransaction *)transcation {
    
}

// 交易延期.
- (void)transcationDeferred:(SKPaymentTransaction *)transcation {
    
}


#pragma mark - SKProductsRequestDelegate

// 查詢成功後的回調.
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
    NSArray<SKProduct *> *products = response.products;
    if (!products.count) {
        NSLog(@"沒有正在出售的商品");
        return;
    }
    
    SKPayment *payment = [SKPayment paymentWithProduct:products.firstObject];
    [[SKPaymentQueue defaultQueue] addPayment:payment];
}

@end
複製代碼

代碼大體作了以下事情,初始化的時候去添加支付結果的監聽,並在 -dealloc: 方法中移除監聽。同時能夠經過 - (void)fetchProductInfoWithProductIdentifiers:(NSSet<NSString *> *)productIdentifiers 方法查詢後臺配置的商品信息。經過 -buyProduction: 方法購買產品,購買成功之後,IAP 經過 - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions 方法通知購買進度。

相關文章
相關標籤/搜索