本文始發(fā)于我的博文 iOS In-App Purchase(IAP) 流程與實(shí)現(xiàn),現(xiàn)轉(zhuǎn)發(fā)至此。
一禀综、前言
最近做了 iOS 應(yīng)用內(nèi)購買握联,踩了很多坑,介紹下流程和簡單的實(shí)現(xiàn)役纹,希望能幫助其他人快速實(shí)現(xiàn)功能偶摔。
可以看我上傳到 Github 的代碼 ZInAppPurchase,或者直接在 CocoaPods 拉取 ZInAppPurchase促脉。(第一次試試上傳到 CocoaPods辰斋,還沒加 demo)策州,也可以使用普遍用到的第三方庫 SwiftyStoreKit。
二亡呵、應(yīng)用內(nèi)購買流程
iOS 應(yīng)用內(nèi)購買流程主要分幾步:
- iTunes Connect 商品配置
- 添加沙盒環(huán)境技術(shù)測試員并登錄
- App 內(nèi)獲取和購買商品
- 驗(yàn)證購買憑證 receipt
- 丟單處理
- 自動續(xù)訂的訂閱商品的處理
- 多 App 賬號同個蘋果賬號的權(quán)益處理/恢復(fù)訂閱
2.1 iTunes Connect 商品配置
主要是填寫完整信息和添加商品抽活。
2.1.1 填寫完整信息
登錄 iTunes Connect,進(jìn)入”協(xié)議锰什、稅務(wù)和銀行業(yè)務(wù)“下硕。
如果 Contracts In Process下有 All(See Contract) 和 Contact Info、Bank Info汁胆、Tax Info 三列梭姓,則表示已填寫;否則點(diǎn)擊 Request 按照提示進(jìn)行操作嫩码。
之后就會出現(xiàn) Contact Info誉尖、Bank Info、Tax Info 三列铸题,分別 Set Up (需要同公司財(cái)務(wù)人員一起填寫)铡恕。
如果沒有填寫完整只能添加免費(fèi)訂閱商品
2.1.2 添加商品
登錄 iTunes Connect,進(jìn)入我的 App——功能——App內(nèi)購買項(xiàng)目丢间,點(diǎn)擊+號探熔。可以添加的類型有:消耗型項(xiàng)目烘挫、非消耗型項(xiàng)目诀艰、自動續(xù)訂訂閱、免費(fèi)訂閱饮六、非續(xù)訂訂閱其垄。商品添加完屏幕快照就會變成準(zhǔn)備提交狀態(tài)。
產(chǎn)品 ID 不可重復(fù)卤橄,如果刪除某個商品绿满,以后這個產(chǎn)品的 ID 也不可用,即使它已經(jīng)被刪除了虽风;另外類型也不能改棒口,選錯了只能重新增加一個商品。
2.2 添加沙盒環(huán)境技術(shù)測試員并登錄
創(chuàng)建沙盒賬戶辜膝,退出手機(jī) App Store 賬戶无牵,登錄沙盒賬戶。
2.2.1 創(chuàng)建沙盒賬戶
登錄 iTunes Connect厂抖,進(jìn)入用戶和職能——沙盒技術(shù)測試員茎毁,點(diǎn)擊+號。
必須是未注冊的 Apple 賬戶,用于測試購買七蜘。
點(diǎn)擊新建的賬號谭溉,可以中斷購買流程、修改訂閱項(xiàng)目續(xù)期率扮念、刪除賬戶,
最好看下每個的說明碧库,有些容易忽略的點(diǎn)柜与,節(jié)省后面的測試時間。
點(diǎn)擊右上角“編輯”嵌灰,再勾選沙盒賬戶弄匕,可以清除購買歷史記錄、刪除賬戶沽瞭。
2.2.2 登錄沙盒賬戶
在手機(jī) App Store 迁匠,點(diǎn)擊右上角按鈕,然后在新頁面一直往下滑驹溃,點(diǎn)擊”退出登錄“城丧。
在手機(jī) 設(shè)置-App Store-沙盒賬戶,登錄創(chuàng)建的沙盒賬戶豌鹤。
如果不操作上面的步驟芙贫,直接 Debug,或者下載使用 TestFlight 的包傍药,默認(rèn)是使用手機(jī)登錄的 App Store 賬戶當(dāng)沙盒賬戶去測試。
這樣會導(dǎo)致一些問題魂仍,已知的問題是訂閱后再點(diǎn)擊訂閱會生成一個去蘋果驗(yàn)證不存在的交易編號拐辽。
而且自己的蘋果賬號作為沙盒賬號,點(diǎn)擊后選擇管理擦酌,會加載不出來或者提示訪問不了俱诸。
2.3 App 內(nèi)獲取和購買商品
- 導(dǎo)入系統(tǒng)庫 StoreKit
import StoreKit
- 獲取商品信息
根據(jù) productId 獲取商品信息(可以獲取多個):
let productRequest = SKProductsRequest(productIdentifiers: Set<String>(arrayLiteral: productId))
productRequest.delegate = self
productRequest.start()
實(shí)現(xiàn) SKProductsRequestDelegate:
func productsRequest(request: SKProductsRequest, didReceiveResponse response: SKProductsResponse) {
if let product = response.products.first {// 獲取返回的商品
}
}
- 購買商品
購買獲取的商品 product:
if SKPaymentQueue.canMakePayments() {// 是否能且允許支付
let payment = SKPayment(product: product)
SKPaymentQueue.defaultQueue().addTransactionObserver(self)
SKPaymentQueue.defaultQueue().addPayment(payment)
}
實(shí)現(xiàn) SKPaymentTransactionObserver:
func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction in transactions {
switch transaction.transactionState {
case .Purchased: // Transaction is in queue, user has been charged. Client should complete the transaction.
if let receiptUrl = NSBundle.mainBundle().appStoreReceiptURL, let receiptData = NSData(contentsOfURL: receiptUrl) {
let receiptString = receiptData.base64EncodedStringWithOptions(NSDataBase64EncodingOptions(rawValue: 0))
// 將receiptString發(fā)給服務(wù)器
}
SKPaymentQueue.defaultQueue().finishTransaction(transaction)
case .Failed: // Transaction was cancelled or failed before being added to the server queue.
if let errorCode = transaction.error?.code {
}
SKPaymentQueue.defaultQueue().finishTransaction(transaction)
default:
break
}
}
}
2.4 驗(yàn)證購買憑證 receipt
憑證驗(yàn)證可以本地驗(yàn)證,也可以發(fā)給服務(wù)器赊舶,由服務(wù)器提交給 App Store 驗(yàn)證睁搭。
參考鏈接:Validating Receipts With the App Store
我們是將 receipt 進(jìn)行 base64 編碼后,傳給服務(wù)器笼平,服務(wù)器判斷憑證是否已經(jīng)存在或驗(yàn)證過园骆,再去 POST 給 Apple 服務(wù)器驗(yàn)證。
服務(wù)器會需要用到“App 專用共享密鑰”寓调,在 appstoreconnect.apple.com - App 信息 可以查看锌唾。
- 沙盒環(huán)境的 URL
https://sandbox.itunes.apple.com/verifyReceipt
- 正式環(huán)境的 URL
https://buy.itunes.apple.com/verifyReceipt
客戶端自己也可以用 Shell 命令測試下看看驗(yàn)證結(jié)果,此處的“password”就是上面所說的“App 專用共享密鑰”。
/// 沙盒環(huán)境
curl -d '{ "exclude-old-transactions": true "password":"yyyy" "receipt-data": "xxxx"}' https://sandbox.itunes.apple.com/verifyReceipt
/// 正式環(huán)境
curl -d '{ "exclude-old-transactions": true "password":"yyyy" "receipt-data": "xxxx"}' https://buy.itunes.apple.com/verifyReceipt
驗(yàn)證后 Apple 會返回?cái)?shù)據(jù)晌涕,從中可以獲取 product_id滋捶、quantity 等,下面是正確時的返回?cái)?shù)據(jù):
{
"status": 0,
"environment": "Sandbox",
"receipt": {
"receipt_type": "ProductionSandbox",
"adam_id": 0,
"app_item_id": 0,
"bundle_id": "com.xxx.xxxxxx",
"application_version": "999",
"download_id": 0,
"version_external_identifier": 0,
"receipt_creation_date": "2016-05-26 04:35:08 Etc/GMT",
"receipt_creation_date_ms": "1464237308000",
"receipt_creation_date_pst": "2016-05-25 21:35:08 America/Los_Angeles",
"request_date": "2016-05-26 06:40:32 Etc/GMT",
"request_date_ms": "1464244832729",
"request_date_pst": "2016-05-25 23:40:32 America/Los_Angeles",
"original_purchase_date": "2013-08-01 07:00:00 Etc/GMT",
"original_purchase_date_ms": "1375340400000",
"original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles",
"original_application_version": "1.0",
"in_app": [
{
"quantity": "1",
"product_id": "000000",
"transaction_id": "1000000213676495",
"original_transaction_id": "1000000213676495",
"purchase_date": "2016-05-26 04:35:08 Etc/GMT",
"purchase_date_ms": "1464237308000",
"purchase_date_pst": "2016-05-25 21:35:08 America/Los_Angeles",
"original_purchase_date": "2016-05-26 04:35:08 Etc/GMT",
"original_purchase_date_ms": "1464237308000",
"original_purchase_date_pst": "2016-05-25 21:35:08 America/Los_Angeles",
"is_trial_period": "false"
}
]
}
}
訂閱的返回?cái)?shù)據(jù):
{
"environment": "Sandbox",
"receipt": {
"receipt_type": "ProductionSandbox",
"adam_id": 0,
"app_item_id": 0,
"bundle_id": "xxx",
"application_version": "202403271640",
"download_id": 0,
"version_external_identifier": 0,
"receipt_creation_date": "2024-03-27 15:17:27 Etc/GMT",
"receipt_creation_date_ms": "1711552647000",
"receipt_creation_date_pst": "2024-03-27 08:17:27 America/Los_Angeles",
"request_date": "2024-03-27 15:18:10 Etc/GMT",
"request_date_ms": "1711552690911",
"request_date_pst": "2024-03-27 08:18:10 America/Los_Angeles",
"original_purchase_date": "2013-08-01 07:00:00 Etc/GMT",
"original_purchase_date_ms": "1375340400000",
"original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles",
"original_application_version": "1.0",
"in_app": [
{
"quantity": "1",
"product_id": "sp_3",
"transaction_id": "2000000556971707",
"original_transaction_id": "2000000556971707",
"purchase_date": "2024-03-27 15:17:26 Etc/GMT",
"purchase_date_ms": "1711552646000",
"purchase_date_pst": "2024-03-27 08:17:26 America/Los_Angeles",
"original_purchase_date": "2024-03-27 15:17:26 Etc/GMT",
"original_purchase_date_ms": "1711552646000",
"original_purchase_date_pst": "2024-03-27 08:17:26 America/Los_Angeles",
"expires_date": "2024-03-27 16:17:26 Etc/GMT",
"expires_date_ms": "1711556246000",
"expires_date_pst": "2024-03-27 09:17:26 America/Los_Angeles",
"web_order_line_item_id": "2000000055757881",
"is_trial_period": "false",
"is_in_intro_offer_period": "false",
"in_app_ownership_type": "PURCHASED"
}
]
},
"latest_receipt_info": [
{
"quantity": "1",
"product_id": "sp_3",
"transaction_id": "2000000556971707",
"original_transaction_id": "2000000556971707",
"purchase_date": "2024-03-27 15:17:26 Etc/GMT",
"purchase_date_ms": "1711552646000",
"purchase_date_pst": "2024-03-27 08:17:26 America/Los_Angeles",
"original_purchase_date": "2024-03-27 15:17:26 Etc/GMT",
"original_purchase_date_ms": "1711552646000",
"original_purchase_date_pst": "2024-03-27 08:17:26 America/Los_Angeles",
"expires_date": "2024-03-27 16:17:26 Etc/GMT",
"expires_date_ms": "1711556246000",
"expires_date_pst": "2024-03-27 09:17:26 America/Los_Angeles",
"web_order_line_item_id": "2000000055757881",
"is_trial_period": "false",
"is_in_intro_offer_period": "false",
"in_app_ownership_type": "PURCHASED",
"subscription_group_identifier": "21443081"
}
],
"latest_receipt": "xxx",
"pending_renewal_info": [
{
"auto_renew_product_id": "sp_3",
"product_id": "sp_3",
"original_transaction_id": "2000000556971707",
"auto_renew_status": "1"
}
],
"status": 0
}
Verify your receipt first with the production URL; then verify with the sandbox URL if you receive a 21007 status code. This approach ensures you don’t have to switch between URLs while your app is in testing, in review by App Review, or live in the App Store.
蘋果官方文檔提到余黎,如果正式環(huán)境驗(yàn)證憑證失敗重窟,收到錯誤碼 21007,則代表該憑證是沙盒環(huán)境的惧财,需要去沙盒環(huán)境驗(yàn)證憑證巡扇。同理,沙盒環(huán)境也有對應(yīng)的錯誤碼可缚。這樣才能不影響審核期間霎迫、測試期間的使用。
2.5 丟單處理
參考官方文檔帘靡,在 didFinishLaunchingWithOptions 的時候知给,調(diào)用 completeTransactions 操作。
具體處理邏輯描姚,不同的支付流程對應(yīng)不同的處理方式涩赢。很多文章都有提到,這里就不贅述了轩勘。
建議設(shè)計(jì)支付流程時筒扒,等用戶支付后才去調(diào)用服務(wù)器。如果在用戶點(diǎn)擊購買時绊寻,調(diào)用服務(wù)器創(chuàng)建自己的訂單花墩,再支付,再通知服務(wù)器澄步,這樣流程長了冰蘑,會更容易出現(xiàn)問題。
2.6 自動續(xù)訂的訂閱商品的處理
在蘋果后臺設(shè)置“App Store 服務(wù)器通知”村缸,在 appstoreconnect.apple.com - App 信息 設(shè)置祠肥,包括生產(chǎn)環(huán)境和測試環(huán)境。
服務(wù)器在配置的 URL 中進(jìn)行邏輯處理梯皿。
2.7 多 App 賬號同個蘋果賬號的權(quán)益處理/恢復(fù)訂閱
權(quán)益跨設(shè)備仇箱、跨 App 賬號使用,是應(yīng)用內(nèi)購買常見且復(fù)雜的問題东羹。
2.7.1 權(quán)益歸屬
如同其他文章所述剂桥,蘋果期望權(quán)益是歸屬于蘋果賬號的,登錄同個蘋果賬號應(yīng)該享用同樣的已購買的權(quán)益百姓。而實(shí)際設(shè)計(jì)時渊额,可能更期望權(quán)益歸屬于 App 賬號的,同個 App 賬號在不同設(shè)備上登錄可以享用相同的已購買的權(quán)益。
不同的產(chǎn)品會設(shè)計(jì)不一樣的邏輯旬迹,跟賬號體系關(guān)聯(lián)火惊。
2.7.2 賬號體系設(shè)計(jì)
在近期提審時,發(fā)現(xiàn)蘋果審核指出奔垦,購買跟賬號無關(guān)的商品時不能要求用戶注冊登錄屹耐,也就是需要支持游客(相對于 App 的賬號體系)購買。即使解釋這種操作是為了方便用戶跨設(shè)備使用也無濟(jì)于事椿猎。這使得整個賬號體系設(shè)計(jì)更復(fù)雜惶岭。
于是整個賬號體系存在三層:設(shè)備、Apple 賬號犯眠、App 賬號按灶,需要進(jìn)行各種登錄和綁定情況的處理。
2.7.3 訂閱
主要有幾種情況需要注意:
賬號b點(diǎn)擊訂閱筐咧,再點(diǎn)擊“已經(jīng)訂閱過”的彈窗上的“好”操作鸯旁,此時權(quán)益其實(shí)還在賬號a上,需要做處理量蕊。另外就是自動續(xù)訂觸發(fā)時铺罢,需要處理續(xù)訂到哪個 App 賬號上。
三残炮、測試
testing_in-app_purchases_with_sandbox
對于自動續(xù)訂的訂閱商品的情況:
如果當(dāng)前蘋果賬號已經(jīng)訂閱韭赘,蘋果會彈出彈窗“已經(jīng)訂閱過”,彈窗上有兩個按鈕“管理”和“好”势就,點(diǎn)擊“管理”會跳轉(zhuǎn)管理頁面泉瞻,并返回失敗(支付取消)結(jié)果苞冯;點(diǎn)擊“好”瓦灶,如果距離自動續(xù)訂時間小于 24 小時,會生成新交易號抱完;如果大于則會返回舊的交易號,屬于重復(fù)訂閱的情況刃泡。
如果此時再點(diǎn)擊“訂閱‘巧娱,不會再彈窗,而是直接返回成功烘贴,效果同上點(diǎn)擊”好“禁添。
下面是在沙盒環(huán)境下的真機(jī)測試截圖(“測試”是所填寫的產(chǎn)品名稱,未登錄Apple ID時會提示登錄桨踪,已登錄時會提示輸入密碼/Touch ID):
四老翘、參考文檔
- Apple Documentation Storekit
- Validating Receipts With the App Store
- testing_in-app_purchases_with_sandbox
-END-
歡迎到我的博客交流:http://zackzheng.info