iOS In-App Purchase(IAP) 流程與實(shí)現(xiàn)

本文始發(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)購買流程主要分幾步:

  1. iTunes Connect 商品配置
  2. 添加沙盒環(huán)境技術(shù)測試員并登錄
  3. App 內(nèi)獲取和購買商品
  4. 驗(yàn)證購買憑證 receipt
  5. 丟單處理
  6. 自動續(xù)訂的訂閱商品的處理
  7. 多 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 InfoBank Info汁胆、Tax Info 三列梭姓,則表示已填寫;否則點(diǎn)擊 Request 按照提示進(jìn)行操作嫩码。
之后就會出現(xiàn) Contact Info誉尖、Bank InfoTax 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 訂閱

主要有幾種情況需要注意:

image.png

賬號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):


IMG_6816.PNG
IMG_6814.PNG

四老翘、參考文檔

-END-
歡迎到我的博客交流:http://zackzheng.info

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子铺峭,更是在濱河造成了極大的恐慌墓怀,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件卫键,死亡現(xiàn)場離奇詭異傀履,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)莉炉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進(jìn)店門钓账,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人絮宁,你說我怎么就攤上這事梆暮。” “怎么了绍昂?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵啦粹,是天一觀的道長。 經(jīng)常有香客問我治专,道長卖陵,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任张峰,我火速辦了婚禮泪蔫,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘喘批。我一直安慰自己撩荣,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布饶深。 她就那樣靜靜地躺著餐曹,像睡著了一般。 火紅的嫁衣襯著肌膚如雪敌厘。 梳的紋絲不亂的頭發(fā)上台猴,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天,我揣著相機(jī)與錄音俱两,去河邊找鬼饱狂。 笑死,一個胖子當(dāng)著我的面吹牛宪彩,可吹牛的內(nèi)容都是我干的休讳。 我是一名探鬼主播,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼尿孔,長吁一口氣:“原來是場噩夢啊……” “哼俊柔!你這毒婦竟也來了筹麸?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤雏婶,失蹤者是張志新(化名)和其女友劉穎物赶,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體尚骄,經(jīng)...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡块差,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了倔丈。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片憨闰。...
    茶點(diǎn)故事閱讀 40,488評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖需五,靈堂內(nèi)的尸體忽然破棺而出鹉动,到底是詐尸還是另有隱情,我是刑警寧澤宏邮,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布泽示,位于F島的核電站,受9級特大地震影響蜜氨,放射性物質(zhì)發(fā)生泄漏械筛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一飒炎、第九天 我趴在偏房一處隱蔽的房頂上張望埋哟。 院中可真熱鬧,春花似錦郎汪、人聲如沸赤赊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽抛计。三九已至,卻和暖如春照筑,著一層夾襖步出監(jiān)牢的瞬間吹截,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工凝危, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留饭弓,地道東北人。 一個月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓媒抠,卻偏偏與公主長得像,于是被迫代替她去往敵國和親咏花。 傳聞我的和親對象是個殘疾皇子趴生,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,500評論 2 359

推薦閱讀更多精彩內(nèi)容