關於蘋果內購(IAP)的一些問題以及那些坑

峻峰飛陽發表於2016-10-20

最近在研究蘋果內購功能,所以,在網上找了一些資料,進行學習。但是,內購功能在實現的過程中,有很多坑,筆者算是真的遇到了好多啊,下面也是自己對內購的一些心得與體會吧!

我這裡說的可能不太詳盡,所以,我先把再網上看到的一些帖子貼在這裡,以便大家做內購的時候,方便查詢相關資訊。

這裡是一篇寫的比較全面的帖子,但是沒有寫中間問題處理: <iOS開發內購全套圖文教程>

在網上搜了一些相關的帖子,簡單歸納總結了一下,覺得論壇裡有一個叫Teng的世界的大神,寫了三篇部落格,寫的很詳細:


【IAP支付之一】In-App Purchase Walk Through 整個支付流程

 

【IAP支付之二】In app purchase 本地購買和伺服器購買兩種購買模式

 

【IAP支付之三】蘋果IAP安全支付與防範 receipt收據驗證

 

大家在做內購之前,推薦看一下!

但是,畢竟我們開發的IAP是在蘋果的平臺上面執行,所以,如果英語能力好的話,最好去蘋果官網無看<官方指南>,裡面涉及到了一些論壇的貼子裡沒有提到過的問題,而這些內容,也很有可能會被大家忽略。下面是<官方文件中文翻譯>,可以對照官方文件檢視。但有時候還會出現相關的問題。好吧,廢話不多說,下面開始說IAP的實現以及具體會遇到的問題,我這裡可能會涉及到好多需要注意的問題,流程性的東西會少一些。大家儘量在讀本篇部落格之前,先把上面的幾個部落格看一下。

首先,我們要去iTunes store建立幾個我們需要在內購中使用到的產品,記住,產品的ID一定要唯一。蘋果官方提到了,IAP購買項有幾種型別:

 

 

Consumable products:消耗類產品

 

Non-consumable products:非消耗類產品

 

Auto-renewable subscriptions:自動更新訂閱產品

 

Non-renewable subscriptions. 非自動更新訂閱產品

 

Free subscriptions. 免費訂閱產

 

 

我們通常再遊戲中用到的遊戲幣屬於消耗類產品,賽車軌道等屬於非消耗類產品,通常這2種會比較常見。我當時用的是消耗類產品。

當完成產品建立之後,去iTunes store申請一個測試賬號,就要開始編寫程式碼了。在編寫程式碼之前,最重要的,是要了解整個內購實現的流程。這裡找到了一個比較好的對<流程解說的帖子>,下面是流程圖:

歸根結底,其實,我們一直在和APP store在打交道,而並不是和蘋果的伺服器進行打交道,所以,大家要避免這個誤區,而APP store才和蘋果伺服器進行打交道,這一層,其實我們基本是不需要考慮的。

流程:

1.首先,從圖上的第一步,客戶端向自己的伺服器傳送了一個請求,請求產品列表,然後,我們自己的伺服器會返回給客戶端產品的identifiers,也就是我們在建立產品的時候,設定的產品ID,當獲取之後,我們需要根據獲得的identifiers向APP store請求產品的詳細資訊。但對於某些應用來說,可能產品種類沒有什麼變動,所以,就直接將identifiers整合在了應用中,有的是直接放在了plist檔案中,需要的時候,直接呼叫,不需要向伺服器傳送請求,獲得訂單資訊。但這樣也有缺點,當產品發生變動的時候,需要釋出新的版本,更新應用才行,所以,不推薦使用這種方案。

2.當獲取了產品資訊之後,要重新整理UI,展示給使用者,讓使用者選擇需要購買那種產品,然後點選購買按鈕。當使用者購買某個產品的時候,我們的APP會向APP store傳送購買請求,APP store接收到,購買請求之後,會進行訂單的處理,然後,返回給我們購買的結果,同時,從上面的途中,我們還可以看到,返回到客戶端有一個receipt data,這個東西其實是用來進行校驗的證書(其實是很長的字串,大概3000多個字元吧),防止有人使用越獄外掛,從而反覆獲取我們的產品,尤其是類似金幣這種。

3.當客戶端獲得購買結果之後,將支付資訊(包括驗證證書)傳送到伺服器,伺服器向AppStore發起驗證,這個驗證必須是post請求,將資料以json格式傳送過去,同時,receipt要進行base64編碼,當蘋果確認之後,會給我們返回狀態碼,告訴我們是否成功。

這是蘋果官方給出的集中狀態值,蘋果返回回來的資料也是json格式的,會有一個state欄位,當為0的時候,表示成功,我們測試的介面是:https://sandbox.itunes.apple.com/verifyReceipt,生產環境的介面是:https://buy.itunes.apple.com/verifyReceipt

,所以大家要區分好這兩個介面。21007表示將測試環境獲得receipt傳送到了生產環境,21008表示將生產環境的receipt傳送到了測試環境下,其他的返回值,應該都表示驗證失敗,但是,具體是什麼,我也不清楚,英語好的話,可以自己翻譯一下,然後,告訴我。這裡是<蘋果官方驗證文件>,大家可以檢視這個,寫出客戶端驗證的程式碼。因為我不是做服務端的,所以不知道怎麼寫服務端驗證,但是,這兩者應該是相通的,大家可以在下面寫評論,一起討論一下。

4.當伺服器從APPstore獲得返回狀態後,判斷是否有這條購買記錄,如果有,就更新伺服器端資料庫,表示物品已經購買,再給客戶端傳送購買結果。這裡說的APPstore,我再網上找了好多資料,都是這麼說的,但我覺得其實就是蘋果伺服器給提供的介面,只不過為了方便,所以,在畫圖的時候,就都畫成了向APP store傳送驗證,其實,這裡是蘋果伺服器提供的一個介面。

筆者公司當時用的是RMStore這個開源庫,這個用著很方便,所以,大家也可以嘗試一下,但不保證完全沒有問題,因為我在使用的過程中,其實也遇到了一些棘手的問題。大家也可以自己寫支付這個模組,其實,正常的這個流程也不是很麻煩,先把基本流程寫完,再考慮可能出現的問題,就會好很多。我在上面引用的幾個連結裡面,有的連結裡面有具體的程式碼,大家可以參考一下。

用RMStore的話,主要會呼叫這樣一個方法:

 

1.- (void)addPayment:(NSString*)productIdentifier
2.user:(NSString*)userIdentifier
3.success:(void (^)(SKPaymentTransaction *transaction))successBlock
4.failure:(void (^)(SKPaymentTransaction *transaction, NSError *error))failureBlock;

先來說說這些引數吧。首先,第一個引數,這個就是我們獲取到的產品的identifier,就是要購買的那個產品的唯一標識;然後是這個user,這個是使用者自定義的一個東西,可以是使用者的UUID或者其他資訊,這個用途很大的。會在後面提到;這裡的success block,實在支付成功後,回撥的內容,只要把成功後進行的操作寫在裡面就可以了,但是,由於成功後,需要的操作也很多,大家一定要把操作封裝一下,在裡面呼叫,否則,邏輯會很亂,而且,下面的failure block中還要對很多異常狀況進行判斷和處理,其中有一個就是“無法連線到iTunes store”,這個問題很麻煩,後面會具體說。一般情況下,如果不考慮user這個變數,可以直接使用下面的方法:

 

1.- (void)addPayment:(NSString*)productIdentifier
2.success:(void (^)(SKPaymentTransaction *transaction))successBlock
3.failure:(void (^)(SKPaymentTransaction *transaction, NSError *error))failureBlock;
  • 這個方法要呼叫上面的方法,但是user預設為nil。

     


     

    支付流程看起來就是這樣,感覺好像很簡單,但是,這裡面的問題其實很大。上面只是在一切都正常的狀態下,才會走的流程,但是,如果考慮到網路問題、斷網、應用閃退,有越獄外掛等問題,問題就麻煩了,這個歷程,各個過程中需要考慮的問題,其實,還是很多的。好的,下面我們就一步一步開始說IAP實現過程中的各種坑。先重新把上面的圖拿過來。

    首先來說第一步:

    這一步還是很輕鬆的,我們向伺服器獲取產品identifiers,由於需要進行網路請求,而且是支付,所以,一定要把斷網考慮進來,這個是必須的,那麼,在這一步,我們要判斷,當沒有獲取資料的時候,要提示使用者暫時沒有獲取產品列表資訊,這部分其實還好,不需要考慮太多。

    之後的一些過程,就比較複雜了,考慮的東西也會比較多了。首先把剩下的部分拿過來:

    這部分問題很多,而且,需要邏輯也很複雜。首先說第七步,這一部分,再應用中,當點選購買的時候,會彈出輸入框,要求輸入賬號和密碼,當點選取消的時候,實際上會呼叫failure block.呼叫failure block的時候,會獲得一條支付資訊transaction和一個error,我們可以判斷transaction的相關資訊,來判斷取消狀態,

    1.if (transaction.error.code == SKErrorPaymentCancelled)
    也就是判斷這個訂單資訊的error的code值,這個就是取消狀態。但實際上,這只是一種比較常見的狀態。當使用者再購買的過程中,如果在這個過程中,突然斷網了,或者請求支付的訂單狀態有問題,也就是上面的過程⑦出現了問題,就會觸發其他的幾種狀態,這個時候,如果只是輸出訂單失敗的資訊,會出現“無法連線到iTunes store”,這是一種很讓人頭疼的狀態,因為,你根本不知道到底是什麼問題,到底是怎麼無法連線到iTunes store。我當時也被這個問題坑了 ,後來發現,這其實是一種請求失敗,和SKErrorPaymentCancelled類似。SKErrorPaymentCancelled和其他幾種狀態其實是列舉型別:

     

     

    01.NS_ASSUME_NONNULL_BEGIN
    02. 
    03.SK_EXTERN NSString * const SKErrorDomain NS_AVAILABLE_IOS(3_0);
    04. 
    05.// error codes for the SKErrorDomain
    06.enum {
    07.SKErrorUnknown,
    08.SKErrorClientInvalid,               // client is not allowed to issue the request, etc.
    09.SKErrorPaymentCancelled,            // user cancelled the request, etc.
    10.SKErrorPaymentInvalid,              // purchase identifier was invalid, etc.
    11.SKErrorPaymentNotAllowed,           // this device is not allowed to make the payment
    12.SKErrorStoreProductNotAvailable,    // Product is not available in the current storefront
    13.};
    14. 
    15.NS_ASSUME_NONNULL_END

    屬於同一類問題,也就是上面說的無法連線到iTunes store,雖然知道了這幾種狀態,但是,還是不知道這幾種狀態到底代表什麼。於是就去蘋果的開發文件裡面看了一下,

     

     

    01.Constants
    02.SKErrorUnknown
    03.Indicates that an unknown or unexpected error occurred.
    04. 
    05.Available in iOS 3.0 and later.
    06.SKErrorClientInvalid
    07.Indicates that the client is not allowed to perform the attempted action.
    08. 
    09.Available in iOS 3.0 and later.
    10.SKErrorPaymentCancelled
    11.Indicates that the user cancelled a payment request.
    12. 
    13.Available in iOS 3.0 and later.
    14.SKErrorPaymentInvalid
    15.Indicates that one of the payment parameters was not recognized by the Apple App Store.
    16. 
    17.Available in iOS 3.0 and later.
    18.SKErrorPaymentNotAllowed
    19.Indicates that the user is not allowed to authorize payments.
    20. 
    21.Available in iOS 3.0 and later.
    22.SKErrorStoreProductNotAvailable
    23.Indicates that the requested product is not available in the store.
    24. 
    25.Available in iOS 6.0 and later.

    這是官方的解釋,可以嘗試翻譯一下,瞭解其代表的含義。後來在網上搜尋了一下相關的文章,只找到一個,說了<無法連線到iTunes store>,但這裡寫的幾種狀態,並沒有全部涵蓋,後來我在網上又找了一下,下面是我給出的對無法連線到iTunes store的處理:

    01.if (transaction.error != nil) {
    02.switch (transaction.error.code) {
    03. 
    04.case SKErrorUnknown:
    05. 
    06.NSLog(@"SKErrorUnknown");
    07.detail = @"未知的錯誤,您可能正在使用越獄手機";
    08.break;
    09. 
    10.case SKErrorClientInvalid:
    11. 
    12.NSLog(@"SKErrorClientInvalid");
    13.detail = @"當前蘋果賬戶無法購買商品(如有疑問,可以詢問蘋果客服)";
    14.break;
    15. 
    16.case SKErrorPaymentCancelled:
    17. 
    18.NSLog(@"SKErrorPaymentCancelled");
    19.detail = @"訂單已取消";
    20.break;
    21.case SKErrorPaymentInvalid:
    22.NSLog(@"SKErrorPaymentInvalid");
    23.detail = @"訂單無效(如有疑問,可以詢問蘋果客服)";
    24.break;
    25. 
    26.case SKErrorPaymentNotAllowed:
    27.NSLog(@"SKErrorPaymentNotAllowed");
    28.detail = @"當前蘋果裝置無法購買商品(如有疑問,可以詢問蘋果客服)";
    29.break;
    30. 
    31.case SKErrorStoreProductNotAvailable:
    32.NSLog(@"SKErrorStoreProductNotAvailable");
    33.detail = @"當前商品不可用";
    34.break;
    35. 
    36.default:
    37. 
    38.NSLog(@"No Match Found for error");
    39.detail = @"未知錯誤";
    40.break;
    41.}
    42.}


     

    這個SKErrorUnknown實在是很難處理,我找了好多的帖子,包括stackoverflow,也沒看到太多的說法,有一些說可能是越獄手機,才會出現這種狀態,在測試的時候,我們通常也會遇到這種問題。測試的時候,我們要再iTunes connect申請測試賬號,有的時候,測試賬號出問題,或者,測試賬號已經被取消了,不再使用了,而支付的時候,仍然在使用這個測試賬號,這個時候,也會出現unknown狀態。

     

    當然,失敗有很多種,這是無法連線到iTunes store,不是網路的問題。上面提到失敗的時候,會有transaction和error兩個返回值,當網路出現問題的時候,error.code是負值。這時,成功的話,沒有這個error資訊,這時,我們就可以判斷到底是怎麼回事了,當返回了error的時候,先判斷transaction.error是否為空,不為空的話,進行上面的switch判斷,為空的話,說明交易的訂單資訊沒有問題,這時候,就只是網路的問題了,就提示使用者網路異常。

    當我們向AppStore傳送了請求之後,如果AppStore交易完成之後,也就是上面的成功的success block,我們首先要將訂單資訊儲存到本地,然後傳送給我們自己的伺服器,當我們的伺服器給我們返回資訊的時候,我們再更新UI,同時,刪除本地儲存的訂單資訊。這個訂單資訊,可以儲存在資料庫中,也可以儲存在檔案中,但是,蘋果建議儲存在檔案中,用NSCoding進行編碼儲存,這樣會更好一些。

    向自己伺服器傳送訊息的話,還要注意很多東西。這裡面也包括AFNetworking的一些問題。但我不瞭解這是不是偶發的事情。當時出現的問題狀況是這樣的:Error Domain=com.alamofire.error.serialization.response Code=-1016 "Request failed: unacceptable content-type: text/html"

     

    1.Error Domain=com.alamofire.error.serialization.response Code=-1016 "Request failed: unacceptable content-type: text/html"
    我當時還不知道這是怎麼回事,後來在網上找了一些資料,才瞭解到,這是AFNetworking對網路請求的資料型別的一種支援問題,下面奉上一篇帖子,告訴大家怎麼解決這個問題:

    Error Domain=com.alamofire.error.serialization.response Code=-1016 "Request failed: unacceptable con


     

    當出現這種問題的時候,訂單資訊會無法上傳到自己的伺服器,這時候,就出問題了,使用者已經支付了,錢已經扣了,但是,我們的伺服器沒有訂單資訊,所以,無法給使用者發貨,類似這樣損害使用者利益的事情是絕對不被允許的。所以,可以按照上面帖子的說法,修改請求型別,新增對text/html的支援,就可以避免這種問題了。此外,當我們自己的伺服器出錯的時候,當使用者打算將訂單資訊上傳到我們伺服器的時候,此時,伺服器可能會返回一些我們預先設定好的狀態碼,對於這種狀態,我們也要在客戶端進行相應的判斷,當遇到這樣的問題的時候,提示用伺服器出錯,趕緊聯絡我們的客服,進行問題的解決。

    上面說到,我們向APP store傳送支付請求的時候,當支付完成的時候,伺服器會將訂單返回給我們,這個時候,我們首先應該做的,其實是將訂單資訊儲存到本地,然後,再向我們自己的伺服器傳送訂單資訊,當伺服器給我們反饋資訊,通知我們成功之後,再刪除本地儲存的訂單資訊。如果失敗的話,我們這裡要設定一個定時器,將未完成的失敗訂單,定時提交到我們的伺服器,從而獲得要購買的商品。但是,如果一直沒有網路怎麼辦?這是,我們就要在每次應用開啟的時候,查詢是否有未完成的訂單資訊,然後將訂單資訊上傳到伺服器,從而獲得我們要購買的商品。

    這種狀態處理完了之後,還有其他的一些狀態,例如,網路狀態不好的狀態下,當我們向APP Store發起訂單請求的時候,請求成功了,但是,當APP Store給我們返回訂單的時候,斷網了,或者,此時退出了應用,以及應用閃退,那該怎麼辦呢?其實,蘋果已經替我們想好了這種問題的解決辦法。我們只要在應用啟動的時候,設定一下代理,就可以了,這是<官方文件>,我們需要在應用啟動的時候,設定SKPaymentQueue的代理方法

     

    1.[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    並實現代理方法
    01.- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
    02.{
    03.// Attach an observer to the payment queue
    04.[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    05.return YES;
    06.}
    07. 
    08.// Called when the application is about to terminate
    09.- (void)applicationWillTerminate:(UIApplication *)application
    10.{
    11.// Remove the observer
    12.[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
    13.}
    當訂單狀態發生狀態的時候,會非同步呼叫這個方法,從而,通知我們更新訂單,並上傳訂單資訊到伺服器,給使用者發貨。如果使用RMStore的話,其實不需要我們手動實現,因為RMstore就是這個observe,所以,在應用啟動的時候,我們就應該對RMSotre這個單例進行初始化。

    下面來說下面這個方法:

     

    1.- (void)addPayment:(NSString*)productIdentifier
    2.user:(NSString*)userIdentifier
    3.success:(void (^)(SKPaymentTransaction *transaction))successBlock
    4.failure:(void (^)(SKPaymentTransaction *transaction, NSError *error))failureBlock;
    這個方法裡裡面有一個user屬性,是用來使用者自定義的欄位,當我們傳送支付請求的時候,傳送這個欄位之後,當獲取了支付成功的請求之後,這個欄位會原封不動的返回回來。當我們用同一個手機,登入了2個不同的賬號的時候,這個欄位就非常有用了。正常情況下,當我們獲得支付訂單資訊之後,要把訂單資訊上傳到自己的伺服器,那麼,怎麼確定就是是哪個使用者呢,預設情況下,我們會把儲存在本地的使用者賬號,也一起返回給自己的伺服器。但是,我們假設這樣一種狀況:我們現在有一個手機,A在上面下單了,訂單已經傳送給APP store,這個時候,斷網了,還沒接到APP store反饋回來的支付結果,這個時候,A退出了賬號,過了一會兒,有網路了,B登入了,這個時候,如果訂單返回了,那麼,我們正常狀態下,需要把這個訂單上傳到我們的伺服器中。那麼,問題來了,我們此時無法或得到下訂單的A的使用者資訊。如果還是按照預設的狀態,此時,會把B的賬號資訊一起傳送到我們的伺服器,這樣,就出錯了,A買的東西,沒得到,B沒有買,卻得到了。這是不合理的。所以,我們要在向APP store傳送支付請求的時候,一起把下單的使用者資訊發過去,也就是儲存在上面的那個user欄位值,當獲得APP store的反饋的時候,再將使用者資訊一起取出來,然後傳送到伺服器,這樣,就不會出現上面說的那種問題了。

     

    以上就是我對最近開發中遇到的一些問題的解決,有不全面的地方和說錯的地方,還請大家批評指點。

相關文章