這兩天因為appstore審核被拒的原因雄嚣,不得不把微信支付寶等支付替換成蘋果內(nèi)購晒屎。經(jīng)過兩天的研究和學(xué)習(xí)我發(fā)現(xiàn)了內(nèi)購的好多個坑,我在這里做了一個總結(jié)缓升,希望能對大家有所幫助鼓鲁,有不對的地方還請大家無情指出并嘲諷之。最后還有我最終的解決方案分享給大家港谊。
一骇吭、內(nèi)購的坑
- app被卸載后使用SKReceiptRefreshReques重新獲取內(nèi)購票據(jù)。
{
SKReceiptRefreshRequest *receiptRefreshRequest = [[SKReceiptRefreshRequest alloc] initWithReceiptProperties:@{}];
receiptRefreshRequest.delegate = self;
[receiptRefreshRequest start];
return;
}
#pragma mark - SKRequestDelegate
- (void)requestDidFinish:(SKRequest *)request{
// 刷新成功票據(jù)的代理
}
這個的坑在于重新獲取的票據(jù)雖然客戶端能驗證成功歧寺,但是驗證成功的信息里面有一個關(guān)鍵字段“in_app”為空數(shù)組燥狰。把這個票據(jù)傳給后臺后是不能驗證通過的,所以就不能為用戶充值成功斜筐,所以這個方法大家慎用龙致。
- finishTransaction
[[SKPaymentQueue defaultQueue] finishTransaction: transaction]
finishTransaction是去告訴蘋果這次交易已經(jīng)結(jié)束,如果不執(zhí)行或者執(zhí)行失斍炅础(為什么說會執(zhí)行失敗呢目代?因為這個方法是異步網(wǎng)絡(luò)請求,網(wǎng)絡(luò)不好的時候就會失斷土贰)下次用戶購買同樣的商品的時候購買成功后會提示“您已經(jīng)免費(fèi)恢復(fù)”的字樣榛了。我看了好多博客,好多博主都說一定要在給用戶充值完以后在執(zhí)行這個方法煞抬,我這里有不同意見霜大,因為如果網(wǎng)絡(luò)不好充值失敗了,沒有執(zhí)行到這個方法的話革答,不僅有上面的問題僧诚,因為沒執(zhí)行成功這個方法的話,下次進(jìn)入app或者進(jìn)入充值界面(取決你在哪里[[SKPaymentQueue defaultQueue] addTransactionObserver:self]
)的時候就會自動回調(diào)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
,這讓我的充值流程變得非常不可控也不可見蝗碎。下面的我的解決方案會提到怎么解決這個問題湖笨。
3.異常訂單存入數(shù)據(jù)庫
數(shù)據(jù)庫是會隨著app卸載而刪除的,我開始沒有想到這一點(diǎn)蹦骑,后來測試發(fā)現(xiàn)了才把這個方案取消了慈省,費(fèi)時又費(fèi)力,希望大家不要踩坑 。
二边败、我的解決方案
內(nèi)購的步驟
不要把問題想得那么復(fù)雜袱衷,大象裝進(jìn)冰箱需要三步呢,可內(nèi)購在我看來就兩步笑窜。
- 用戶付錢給蘋果完畢
- 客戶端發(fā)送請求給服務(wù)器驗證票據(jù)進(jìn)行充值成功致燥。
思考?
- 什么時候會漏單呢排截?很簡單嫌蚤,第一步完成,第二步?jīng)]完成断傲。
- 什么時候第一步完成呢脱吱?因該是在
paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
里面transaction.transactionState = SKPaymentTransactionStatePurchased
。- 什么時候第二步?jīng)]完成认罩?不管什么原因箱蝠,肯定沒來到充值接口成功的回調(diào)里面。
開始解決
用戶付錢給蘋果完畢 使用鑰匙串(鑰匙串不會隨著app卸載而刪除垦垂,會永久性的存儲宦搬,除非用戶換手機(jī),我這里使用的是SAMKeychain)存儲用戶付款的憑證(receipt)和交易id(transactionId)劫拗。儲存憑證在付款成功的回調(diào)里面间校,存儲的時候要區(qū)分用戶,所以監(jiān)聽蘋果隊列的代碼
[[SKPaymentQueue defaultQueue] addTransactionObserver:self]
要寫在登錄之后杨幼,否則無法正確存儲憑證。充值接口成功的回調(diào)里面刪除用戶付款的憑證(receipt)和交易id(transactionId)聂渊。
由于各種原因?qū)е鲁渲凳÷﹩瘟嗽趺崔k差购?在用戶再次想充值的時候,判斷鑰匙串中有沒有存儲的數(shù)據(jù)汉嗽,如果有的話給一個攔截的操作欲逃,我這邊是給一個彈窗,提示用戶處理異常訂單饼暑。這里很關(guān)鍵稳析,這個操作保證了用戶在進(jìn)行內(nèi)購操作的時候,最多只能有一個異常訂單所以我們不擔(dān)心有串單的風(fēng)向弓叛,也不會有大量漏單的情況出現(xiàn)彰居。點(diǎn)擊彈窗的按鈕“立即處理異常訂單”會從鑰匙串中獲取之前存儲的憑證和交易id繼續(xù)向公司的服務(wù)器發(fā)起驗證并充值,如果充值成功刪除憑證和交易id撰筷。如果失敗了陈惰,不用進(jìn)行任何操作。當(dāng)用戶點(diǎn)擊充值的時候還是會有一個彈窗提示毕籽。
finishTransaction的調(diào)用抬闯,上面的坑說到了這個方法井辆。所以我們?yōu)榱四軌蜃约嚎刂瞥渲盗鞒蹋瑪[脫蘋果的控制溶握,我們不用在乎他什么時候調(diào)用杯缺,但一定盡可能保證他調(diào)用完成。我是怎么做的呢睡榆?
- 用戶付錢給蘋果完畢調(diào)用一次萍肆。
- 充值成功調(diào)用一次。
- 充值失敗調(diào)用一次肉微。
- 充值前把當(dāng)前所有的transaction進(jìn)行finish操作匾鸥。
如果這樣還不能避免由于網(wǎng)絡(luò)等原因沒有執(zhí)行完[[SKPaymentQueue defaultQueue] finishTransaction: transaction]
怎么辦呢?這我也做了處理碉纳。我加了一個屬性isNewtranstion
,意思就是這個交易是新的嗎勿负,或者換句話說,這個transtion(交易)是從buyProduct:(SKProduct *)productIdentifier onCompletion:(IAPbuyProductCompleteResponseBlock)completion
這個方法中來的嗎劳曹?
所以我在這個方法中為這個字段進(jìn)行了賦值操作奴愉。
- (void)buyProduct:(SKProduct *)productIdentifier onCompletion:(IAPbuyProductCompleteResponseBlock)completion {
SKPayment *payment = [SKPayment paymentWithProduct:productIdentifier];
if ([SKPaymentQueue defaultQueue]) {
[[SKPaymentQueue defaultQueue] addPayment:payment];
[CHBUserDataCenterModel sharedInstance].isNewtranstion = YES;
}
}
如果是舊的交易,就是沒有被finish的交易的話我就再finish一下(我就不信干不了你L酢6稹!)
if (![CHBUserDataCenterModel sharedInstance].isnewtranstion) {
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
return;
}
但是這么做會引發(fā)一個問題蜕劝,用戶付錢給蘋果完畢之前殺死app檀头,但是因為是發(fā)送給蘋果的請求還是會彈出您的購買已完成,就無法響應(yīng)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
中將憑證和交易id保存到鑰匙串的操作了岖沛,所以要把保存放到第一步暑始,而且要加判斷,防止憑證或者交易id為空的transtion進(jìn)入回調(diào)壞事婴削。
// 獲取小票
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
// 從沙盒中獲取到購買憑據(jù)
NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];
NSString *payload = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
NSString* payload1 = [SAMKeychain passwordForService:receiptservice account:[CHBUserDataCenterModel sharedInstance].passportId];
NSString* transactionId1 = [SAMKeychain passwordForService:transactionIdservice account:[CHBUserDataCenterModel sharedInstance].passportId];
if (payload1 == nil) {
BOOL issaved = [SAMKeychain setPassword:payload forService:receiptservice account:[CHBUserDataCenterModel sharedInstance].passportId];
while (!issaved) {
[SAMKeychain setPassword:payload forService:receiptservice account:[CHBUserDataCenterModel sharedInstance].passportId];
}
}
if (transactionId1 == nil){
BOOL issaved1 = [SAMKeychain setPassword:transaction.transactionIdentifier forService:transactionIdservice account:[CHBUserDataCenterModel sharedInstance].passportId];
while (!issaved1) {
[SAMKeychain setPassword:transaction.transactionIdentifier forService:transactionIdservice account:[CHBUserDataCenterModel sharedInstance].passportId];
}
}
- 還有一個問題:調(diào)用充值接口的時候廊镜,app被殺死了。
這種情況的話會產(chǎn)生一種現(xiàn)象唉俗,post請求發(fā)出了嗤朴,后臺接收到了,充值成功了虫溜,但是客戶端收不到響應(yīng)了雹姊,也不能執(zhí)行充值成功后的代碼了。這時需要后臺提供一個接口:驗證鑰匙串中的憑證和交易id充值成功過沒有衡楞,如果充值成功過容为,就刪除鑰匙串中內(nèi)容,避免用戶充值的時候有個提示框攔截操作,如果沒有充值成功過坎背,那就是多慮了替劈,還是按之前的流程走。
三得滤、相關(guān)思路
-
內(nèi)購流程圖
流程圖.png -
內(nèi)購測試點(diǎn)
測試點(diǎn).png