這篇文章最開始也是由我發表於CSDN,鏈接如下:關於蘋果內購(IAP)的一些問題以及那些坑
希望大家轉載時,注明出處!!!!!
最近公司做的項目,里面用到了內購功能,所以,在網上找了一些資料,研究內購功能的實現,但是,內購功能在實現的過程中,有很多坑,筆者算是真的遇到了好多啊,這也多虧了公司的測試團隊,在項目上線前把這些問題測出來了!下面也是自己對內購的一些心得與體會吧!
我這里說的可能不太詳盡,所以,我先把再網上看到的一些帖子貼在這里,以便大家做內購的時候,方便查找相關信息。
這里是一篇寫的比較全面的帖子,但是沒有寫中間問題處理: <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的話,主要會調用這樣一個方法:
- (void)addPayment:(NSString*)productIdentifier user:(NSString*)userIdentifier success:(void (^)(SKPaymentTransaction *transaction))successBlock failure:(void (^)(SKPaymentTransaction *transaction, NSError *error))failureBlock;
先來說說這些參數吧。首先,第一個參數,這個就是我們獲取到的產品的identifier,就是要購買的那個產品的唯一標識;然后是這個user,這個是用戶自定義的一個東西,可以是用戶的UUID或者其他信息,這個用途很大的。會在后面提到;這里的success block,實在支付成功后,回調的內容,只要把成功后進行的操作寫在里面就可以了,但是,由於成功后,需要的操作也很多,大家一定要把操作封裝一下,在里面調用,否則,邏輯會很亂,而且,下面的failure block中還要對很多異常狀況進行判斷和處理,其中有一個就是“無法連接到iTunes store”,這個問題很麻煩,后面會具體說。一般情況下,如果不考慮user這個變量,可以直接使用下面的方法:
- (void)addPayment:(NSString*)productIdentifier success:(void (^)(SKPaymentTransaction *transaction))successBlock failure:(void (^)(SKPaymentTransaction *transaction, NSError *error))failureBlock;
這個方法要調用上面的方法,但是user默認為nil。
支付流程看起來就是這樣,感覺好像很簡單,但是,這里面的問題其實很大。上面只是在一切都正常的狀態下,才會走的流程,但是,如果考慮到網絡問題、斷網、應用閃退,有越獄插件等問題,問題就麻煩了,這個歷程,各個過程中需要考慮的問題,其實,還是很多的。好的,下面我們就一步一步開始說IAP實現過程中的各種坑。先重新把上面的圖拿過來。
首先來說第一步:
這一步還是很輕松的,我們向服務器獲取產品identifiers,由於需要進行網絡請求,而且是支付,所以,一定要把斷網考慮進來,這個是必須的,那么,在這一步,我們要判斷,當沒有獲取數據的時候,要提示用戶暫時沒有獲取產品列表信息,這部分其實還好,不需要考慮太多。
之后的一些過程,就比較復雜了,考慮的東西也會比較多了。首先把剩下的部分拿過來:
這部分問題很多,而且,需要邏輯也很復雜。首先說第七步,這一部分,再應用中,當點擊購買的時候,會彈出輸入框,要求輸入賬號和密碼,當點擊取消的時候,實際上會調用failure block.調用failure block的時候,會獲得一條支付信息transaction和一個error,我們可以判斷transaction的相關信息,來判斷取消狀態,
if (transaction.error.code == SKErrorPaymentCancelled)
也就是判斷這個訂單信息的error的code值,這個就是取消狀態。但實際上,這只是一種比較常見的狀態。當用戶再購買的過程中,如果在這個過程中,突然斷網了,或者請求支付的訂單狀態有問題,也就是上面的過程⑦出現了問題,就會觸發其他的幾種狀態,這個時候,如果只是輸出訂單失敗的信息,會出現“無法連接到iTunes store”,這是一種很讓人頭疼的狀態,因為,你根本不知道到底是什么問題,到底是怎么無法連接到iTunes store。我當時也被這個問題坑了 ,后來發現,這其實是一種請求失敗,和SKErrorPaymentCancelled類似。SKErrorPaymentCancelled和其他幾種狀態其實是枚舉類型:
NS_ASSUME_NONNULL_BEGIN SK_EXTERN NSString * const SKErrorDomain NS_AVAILABLE_IOS(3_0); // error codes for the SKErrorDomain enum { SKErrorUnknown, SKErrorClientInvalid, // client is not allowed to issue the request, etc. SKErrorPaymentCancelled, // user cancelled the request, etc. SKErrorPaymentInvalid, // purchase identifier was invalid, etc. SKErrorPaymentNotAllowed, // this device is not allowed to make the payment SKErrorStoreProductNotAvailable, // Product is not available in the current storefront }; NS_ASSUME_NONNULL_END
屬於同一類問題,也就是上面說的無法連接到iTunes store,雖然知道了這幾種狀態,但是,還是不知道這幾種狀態到底代表什么。於是就去蘋果的開發文檔里面看了一下,
Constants SKErrorUnknown Indicates that an unknown or unexpected error occurred. Available in iOS 3.0 and later. SKErrorClientInvalid Indicates that the client is not allowed to perform the attempted action. Available in iOS 3.0 and later. SKErrorPaymentCancelled Indicates that the user cancelled a payment request. Available in iOS 3.0 and later. SKErrorPaymentInvalid Indicates that one of the payment parameters was not recognized by the Apple App Store. Available in iOS 3.0 and later. SKErrorPaymentNotAllowed Indicates that the user is not allowed to authorize payments. Available in iOS 3.0 and later. SKErrorStoreProductNotAvailable Indicates that the requested product is not available in the store. Available in iOS 6.0 and later.
這是官方的解釋,可以嘗試翻譯一下,了解其代表的含義。后來在網上搜索了一下相關的文章,只找到一個,說了<無法連接到iTunes store>,但這里寫的幾種狀態,並沒有全部涵蓋,后來我在網上又找了一下,下面是我給出的對無法連接到iTunes store的處理:
if (transaction.error != nil) { switch (transaction.error.code) { case SKErrorUnknown: NSLog(@"SKErrorUnknown"); detail = @"未知的錯誤,您可能正在使用越獄手機"; break; case SKErrorClientInvalid: NSLog(@"SKErrorClientInvalid"); detail = @"當前蘋果賬戶無法購買商品(如有疑問,可以詢問蘋果客服)"; break; case SKErrorPaymentCancelled: NSLog(@"SKErrorPaymentCancelled"); detail = @"訂單已取消"; break; case SKErrorPaymentInvalid: NSLog(@"SKErrorPaymentInvalid"); detail = @"訂單無效(如有疑問,可以詢問蘋果客服)"; break; case SKErrorPaymentNotAllowed: NSLog(@"SKErrorPaymentNotAllowed"); detail = @"當前蘋果設備無法購買商品(如有疑問,可以詢問蘋果客服)"; break; case SKErrorStoreProductNotAvailable: NSLog(@"SKErrorStoreProductNotAvailable"); detail = @"當前商品不可用"; break; default: NSLog(@"No Match Found for error"); detail = @"未知錯誤"; break; } }
這個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"
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的代理方法
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
並實現代理方法
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Attach an observer to the payment queue [[SKPaymentQueue defaultQueue] addTransactionObserver:self]; return YES; } // Called when the application is about to terminate - (void)applicationWillTerminate:(UIApplication *)application { // Remove the observer [[SKPaymentQueue defaultQueue] removeTransactionObserver:self]; }
當訂單狀態發生狀態的時候,會異步調用這個方法,從而,通知我們更新訂單,並上傳訂單信息到服務器,給用戶發貨。如果使用RMStore的話,其實不需要我們手動實現,因為RMstore就是這個observe,所以,在應用啟動的時候,我們就應該對RMSotre這個單例進行初始化。
下面來說下面這個方法:
- (void)addPayment:(NSString*)productIdentifier user:(NSString*)userIdentifier success:(void (^)(SKPaymentTransaction *transaction))successBlock 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的反饋的時候,再將用戶信息一起取出來,然后發送到服務器,這樣,就不會出現上面說的那種問題了。
以上就是我對最近開發中遇到的一些問題的解決,有不全面的地方和說錯的地方,還請大家批評指點。