我們知道,蘋果允許用戶使用Apple ID在應(yīng)用程序中購買商品和服務(wù)。 所有的應(yīng)用內(nèi)購買都需要驗證。 有兩種方法可以驗證這些購買:
1)通過App Store,通過您的應(yīng)用程序和您的服務(wù)器之間的安全連接,或
2)本地。
本地驗證可用于不需要服務(wù)器的簡單應(yīng)用程序。 但在這里你遇到安全風(fēng)險:本地購買可以偽造的黑客iPhone。 因此,使用自己的服務(wù)器與App Store進行通信通常是最好的選擇。 在這種情況下,您的應(yīng)用程序?qū)⒅蛔R別并信任您的服務(wù)器,讓您控制服務(wù)器和用戶設(shè)備之間的所有交易。
在本文中,我們將通過我們的服務(wù)器分享我們驗證購買的專業(yè)知識。 我們在我們的一個項目中實現(xiàn)了這個功能,一個約會應(yīng)用程序叫做Bro。
什么是Bro?
Bro是一款男性同性戀交友APP。 Bro具有豐富的功能,并提供兩種類型的應(yīng)用內(nèi)購買:
. 一個月,六個月和每年訂閱。 訂閱者獲得無廣告的體驗,可以看到更多的潛在匹配,并在本地用戶中顯示更多(最多200個)匹配。
. 一個“Bromance”功能,像Tinder的超級喜歡。
當(dāng)我們在Bro中使用應(yīng)用內(nèi)購買時,我們必須處理一些我們想要與您分享的挑戰(zhàn)。 產(chǎn)品的應(yīng)用程序提供包括高級訂閱(可再生),高級訂閱(不可再生),消費品(bromances)。
在我們開始開發(fā)我們的采購系統(tǒng)之前,我們研究了使用我們自己的服務(wù)器進行驗證的好處。 這里是我們想出了:
- 服務(wù)器端驗證比本地驗證更安全。
- 我們已經(jīng)有了自己的服務(wù)器。
- 如果用戶是管理員,他們可以訪問某些高級功能,而無需購買。
- 由于我們同時擁有Android和iOS應(yīng)用程序,我們需要在兩個操作系統(tǒng)上跟蹤高級用戶狀態(tài),這在單個服務(wù)器上最方便。
關(guān)于購買收據(jù)的一些特殊信息:
收據(jù)是包含有關(guān)購買的所有信息的文件,包括購買日期,到期日期,原始購買ID,產(chǎn)品ID等。
蘋果最近推出了新的收據(jù)格式 - 通用收據(jù)。 以前,您將收到每個交易的單獨收據(jù)(即一個交易等于一個收據(jù))。 但現(xiàn)在,他們只發(fā)送一個收據(jù),其中包含有關(guān)給定Apple ID的所有購買的所有信息,包括已完成和未完成的購買。
通用收據(jù)的另一個變化是,它們是從mainBundle()使用appStoreReceiptURL變量下載的,而不是來自事務(wù)本身。
Flow:
用戶購買訂閱或可消費產(chǎn)品。 當(dāng)購買完成,蘋果發(fā)送一個收據(jù),我們可以從mainBundle()訪問當(dāng)我們收到收據(jù),我們?nèi)缓笞鳛橐恍写a發(fā)送到我們的服務(wù)器。
let mainBundle = NSBundle.mainBundle()
let receiptUrl = mainBundle.appStoreReceiptURL
let isPresent = receiptUrl?.checkResourceIsReachableAndReturnError(nil)
if isPresent == true, let receiptUrl = receiptUrl, receipt = NSData(contentsOfURL: receiptUrl) {
let receiptData = receipt.base64EncodedStringWithOptions(NSDataBase64EncodingOptions(rawValue: 0))
}
//receiptData - encoded string for our server
當(dāng)此收據(jù)到達我們的服務(wù)器時,它由App Store驗證,并且如果有效,則將信息發(fā)送到客戶端的設(shè)備,例如關(guān)于在服務(wù)器側(cè)的客戶端狀態(tài)的改變,或關(guān)于由 客戶。
在Bro應(yīng)用程序中,當(dāng)客戶端成為高級用戶或支付附加服務(wù)時,這些操作將通過服務(wù)器運行。 這意味著,有關(guān)用戶購買狀態(tài)的所有信息(例如,他們是否有高級帳戶)在服務(wù)器上始終是最新的。
但作為一個附加層 - 服務(wù)器 - 是在客戶端和購買之間引入的,我們必須考慮購買已經(jīng)完成但服務(wù)器尚未收到任何信息的情況 - 用戶已經(jīng)關(guān)閉了 app。 例如,如果互聯(lián)網(wǎng)連接中斷,或者設(shè)備的電池電量耗盡,則可能會發(fā)生這種情況。
蘋果在其技術(shù)文檔中明確指出,在購買之后,其狀態(tài)必須設(shè)置為“完成”,然后過程才能被視為完成。 在我們的情況下,我們只能在客戶端設(shè)備收到來自服務(wù)器的響應(yīng)后,將購買標記為“已完成”,表明服務(wù)器成功傳輸和驗證了所有數(shù)據(jù)。
private func validateReceipt(transaction: SKPaymentTransaction, isRestoring: Bool = false) {
let mainBundle = NSBundle.mainBundle()
let receiptUrl = mainBundle.appStoreReceiptURL
let isPresent = receiptUrl?.checkResourceIsReachableAndReturnError(nil)
if isPresent == true, let receiptUrl = receiptUrl, receipt = NSData(contentsOfURL: receiptUrl) {
let receiptData = receipt.base64EncodedStringWithOptions(NSDataBase64EncodingOptions(rawValue: 0))
configureRestoringRequest( parameters, completion: {
SKPaymentQueue.defaultQueue().finishTransaction(transaction) // finish transaction only when response from server is received
} )
} else {
// handle case when there is no receipt data
}
}
private func configureValidationRequest(receiptData: String, completion: () -> ()) {
APIClient.defaultClient().validatePremium( receiptData, success: {[weak self] _, _ in completion()
}, failure: {[weak self] _, error in
// handle errors here
} )
}
如果購買的狀態(tài)為已購買或已恢復(fù),則該狀態(tài)將保留在付款隊列中進行處理,直到狀態(tài)更改為已完成或已取消。
func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .Purchased: // handle transaction is Purchased if needed case .Restored: // handle transaction is Restored if needed
case .Purchasing: // handle transaction purchasing is in progress if needed case .Failed: // handle transaction failure if needed
default:
break
}
}
}
這就是為什么我們在每次啟動應(yīng)用程序時激活我們的Purchase服務(wù),所以我們可以處理由于某種原因或沒有成功發(fā)送到服務(wù)器的任何交易。
// call this function when your session starts
private func setupPurchaseService() {
BroPurchaseService.sharedPurchasingService.setupPurchasingService()
}
private func checkStoreAvailability() {
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
if SKPaymentQueue.canMakePayments() {
let productID = NSSet(objects: productOneId, productTwoId, etc) // add all products ids
let request = SKProductsRequest(
productIdentifiers: productID as! Set<String>)
request.delegate = self
request.start()
} else {
// handle cases when your app can't make payments
}
}
此外,通過App Store的所有可續(xù)訂訂閱在驗證階段請求共享密鑰。 在驗證期間,此密鑰必須與收據(jù)一起發(fā)送,并且此密鑰對于應(yīng)用程序的所有用戶都是相同的。 為了節(jié)省時間并避免將密鑰從客戶端發(fā)送到服務(wù)器,我們將其存儲在服務(wù)器上,并僅發(fā)送回執(zhí)。 這比為每個事務(wù)將密鑰發(fā)送到服務(wù)器更安全。 如果密鑰必須被替換(出于安全原因,例如如果數(shù)據(jù)被泄露),則管理員可以通過生成新密鑰并且使用它們自己的簡檔用新密鑰替換舊密鑰來替換密鑰。
當(dāng)用戶成功購買產(chǎn)品時,服務(wù)器將讓我們知道它,無論他們登錄什么設(shè)備。 此外,服務(wù)器將自動檢查其在App Store上的訂閱狀態(tài),并在高級功能已過期時限制高級功能。
有關(guān)恢復(fù)訂閱的一些提示:
當(dāng)您恢復(fù)購買時,所有已完成的交易都將轉(zhuǎn)到具有已恢復(fù)狀態(tài)的付款隊列。 這意味著我們必須驗證并嘗試恢復(fù)每個事務(wù); 但如果我們在SandBox環(huán)境中還原它們,則可能需要5分鐘才能續(xù)訂1個月的訂閱,并且可能需要30分鐘才能續(xù)訂6個月的訂閱。 6個不同的交易可能需要30分鐘!
我們決定在恢復(fù)每個交易時不發(fā)送請求,而是向包含所有購買信息的服務(wù)器發(fā)送一個收據(jù)。 然后,Apple在其服務(wù)器上驗證收據(jù),檢查所有事務(wù)并恢復(fù)那些需要恢復(fù)的事務(wù)。 一旦服務(wù)器發(fā)送確認交易成功,所有先前狀態(tài)為Restored的交易將收到狀態(tài)完成。
func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
var restoreBegan = false
for transaction in transactions {
switch transaction.transactionState {
case .Purchased: // handle transaction is Purchased if needed
case .Restored:
if !restoreBegan {
restoreBegan = true validateReceipt(transaction) // validate receipt as it is purchased
} else {
SKPaymentQueue.defaultQueue().finishTransaction(transaction)
}
case .Purchasing: // handle transaction purchasing is in progress if needed
case .Failed: // handle transaction failure if needed
default:
break
}
}
}
此外,如果用戶嘗試在嘗試失敗后再次嘗試購買訂閱,Apple將替換地提供這些訂閱以恢復(fù)其現(xiàn)有訂閱,而不是完成新購買。 還有一個不尋常的情況需要考慮:如果用戶嘗試購買兩次產(chǎn)品,它會再次顯示在付款隊列中,但也會恢復(fù),這意味著用戶不會被收取新的購買費用。
蘋果的新通用收據(jù)允許我們通過只向服務(wù)器發(fā)送一個請求來執(zhí)行所有操作,因為它包含所有以前事務(wù)的所有信息。
我們還想提到一些我們注意到的常見的沙盒陷阱:
- 在我們的其中一個測試設(shè)備上,我們有時會收到來自多個測試帳戶的交易,包括已刪除Apple ID的帳戶。
- 收據(jù)可能大到100 kb(當(dāng)與某些類型的服務(wù)器交互時會導(dǎo)致麻煩)。
- 一些購買結(jié)果是恢復(fù)狀態(tài),雖然這不應(yīng)該發(fā)生根據(jù)項目的技術(shù)規(guī)格。
- 蘋果服務(wù)器的響應(yīng)時間很慢。
- 有時,購買沒有出現(xiàn)在隊列中 - 我們沒有得到蘋果的任何回調(diào)。
此外,根據(jù)我們的經(jīng)驗,我們建議在一臺設(shè)備上使用一個Apple ID測試一個用戶。
讓我們總結(jié)一下我們在應(yīng)用內(nèi)購買的經(jīng)驗。
使用App Store驗證收據(jù)相對于本地驗證具有以下優(yōu)點:
- 所有信息在服務(wù)器端驗證,這意味著無論運行應(yīng)用程序的設(shè)備是什么,用戶將獲得有關(guān)購買的最新數(shù)據(jù)。
- 服務(wù)器還會跟蹤所有購買,以確保一次購買(在我們的情況下是應(yīng)用的訂閱)只使用一次。 換句話說,一次購買不能結(jié)束記入多個帳戶。