IAP (In-App Purchase) 內購功能

法蘭克的 iOS 世界
13 min readMay 6, 2017

--

Apple 做了一個統計,開發者有 70% 的收益都是來自於內購的功能所獲得,而內購這個行為簡單來說就是在 APP 裡從事購買的動作,例如購買遊戲的裝備、解鎖特殊的功能、購買電子書等等。而在開始說明之前首先了解一下有關於內購的一些基本知識。

  1. 內購的種類:
  • 消耗性項目( Consumable )=> 只可使用一次的產品,使用一次後便失效,必須再次購買。例如:釣魚 App 中的魚飼料。
  • 非消耗性項目( Non-Consumable )=> 只需購買一次,並且不會過期或是不隨著使用減少的產品。例如:遊戲 App 中的比賽賽道。
  • 自動續訂型訂閱( Renewable Subscriptions )=> 允許使用者在一段固定的期間內購買動態內容的產品。此訂閱種類除非使用者選擇取消,否則會自動續訂。例如:每月訂閱 App 提供串流服務。
  • 非續訂型訂閱( Non-Renewable Subscriptions )=> 允許使用者在一段有限的期間內購買服務的產品。此 App 內購買項目的內容可為固定的。此訂閱種類不會自動續訂。例如:每年訂閱已封存文章的目錄。

2. 內購的限制:

  • 「非實體產品」都必須透過「內購」購買
  • 「實體產品」可透過「第三方支付」購買

3. 若是有「非消耗性項目」( Non-Consumable )的內購項目,則必須提供「恢復購買」的功能,否則 APP 送審時可是會被駁回的。

4. 若是有「自動續訂型訂閱( Renewable Subscriptions ),則有以下幾點要特別注意:

  • 若是沒有出現該選項,則有可能是沒有去設定「協議、稅務與銀行業務」,可參考這裡設定。
  • 在 iTunes Connect 設定自動型訂閱時必須設定群組,同一個群組內的商品只能訂購一個,意思就是說必須等到一個到期了,才能夠再購買同一個群組內的另一個商品。
  • 在測試環境測試自動續訂是會有壓縮時間的,可以參考 Apple 的相關文件
Auto-Renewable compress time table for testing environment

5. 若是成功交易(實體商品除外),Apple 會向開發者收取 30% 的手續費,例如使用者購買了 $30 元的商品,則我們實際只會收到 $21。

EX:自動續訂的產品項目在第二年開始只會收取 15% 的手續費。

【前置作業】

  1. 必須要有開發者的帳號,因必須登入 iTunes Connect 做相關的設定。
  2. 登入 iTunes Connect 並已設定好「協議、稅務與銀行業務」相關項目(包含帳戶資料等等),否則會無法使用 IAP 項目。

3. 必須要有 iPhone 實機一台,因要在實機才有辦法測試 IAP。

【內容大綱】

  1. 在 Xcode 新增一個 Single View Application 的專案,並開啟 IAP 功能。
  2. 在 iTunes Connect 申請「沙箱測試人員帳號」。
  3. 在 iTunes Connect 新增一個拿來測試 IAP 的 APP。
  4. 在 iTunes Connect 新增兩個 IAP 項目。
  5. 在專案裡取得內購所有的產品資訊。
  6. 實作購買消耗性、非消耗性產品、回復購買的動作。

在開始之前先來看一下 StoryBoard 的畫面和整個專案的架構,如此在開始實作前會比較清楚。

StoryBoard 畫面

Controller:

  • ViewController => 第一個載入的主畫面。
  • IAPViewController => 點擊第一個畫面 LeftBarButton 購買按鈕所開啟的畫面。

View:

  • IAPTableViewCell => Custom 購買頁面的 UITableViewCell。

Delegate:

  • IAPurchaseViewControllerDelegate => 購買頁面的 delegate,用來告知主頁面說使用者購買了什麼產品。

以上為整個專案的架構,Storyboard 的部分請自行建立,或直接下載完整範例。在開始實作前,先簡單說明一下範例的流程,該範例主頁面有「目前金幣」和「聊天功能」兩個項目,目前金幣可透過購買「消耗性產品」來做累加的動作,而聊天功能則是會透過購買「非消耗性產品」來做永久性的開放。而非消耗性產品是跟隨著 Apple ID 的,意思是說假設我們換了一支手機,並且使用之前購買過該產品的 Apple ID 來登入的話,就可以透過「回復購買」的動作來重新開放「聊天功能」,並不需要再重覆購買的。而整個測試過程所使用的帳號都是僅供測試用的,並不會真的付費。以下就來看看今天的成品影片。

在 Xcode 新增一個 Single View Application 的專案,並開啟 IAP 功能

為什麼要先新增一個專案並開啟 IAP 的功能呢?因為如果沒有開啟 IAP 的功能的話,在 iTunes Connect 要新增 APP 時會找不到該 APP 的 Bundle ID,若還是不太了解的沒關係,先照著這樣做,在接下來的步驟就會了解了。

  1. 專案 → TARGETS → Capabilities → In-App Purchase → on

2. 確認在 Signing 的設定已選擇有開發者權限的帳號

在 iTunes Connect 申請「沙箱測試人員帳號」

在測試付款的過程去,當然不可能真的去付費,這也太傷財了吧!所以我們必須申請測試的帳號以用來測試 IAP 的功能。

  1. 登入 iTunes Connect 並選擇「使用者和職能」

2. 選擇「沙箱技術測試人員」的頁籤,並點選「+」號來開始新增測試帳號

3. 填入相關的資訊後,最後點選「儲存」即可

在 iTunes Connect 新增一個拿來測試 IAP 的 APP

  1. 點選我的 APP 後,並點選「+」號

2. 填入該 APP 的相關資訊

套裝組 ID => 該 ID 必須選擇在第一步驟所產生的 Bundle ID,在第一步驟若是沒有開啟 IAP 的功能,這邊則無法選到該套裝組 ID。

在 iTunes Connect 新增兩個 IAP 項目

這邊我們要申請兩個 IAP 項目,一個是「消耗性產品」,另一個則是「非消耗性產品」。

  1. 點擊剛剛生成的 APP

2. 點擊功能的頁籤,並點擊「+」號來開始新增「消耗性項目」

3. 填入相關的欄位資訊

若是有不清楚欄位的對應關係,點擊欄位名稱旁的「?」都會有貼心的說明

「顯示名稱」和「描述」這兩個欄位會顯示在畫面上給使用者看,所以必須清楚的填入,最後點選「儲存」即可。

4. 同樣的方式新增一個「非消耗性項目」

在專案裡取得內購所有的產品資訊

都定義好了之後,可以回到專案裡開始來實作取得上一步驟在 iTunes Connect 所定義的產品資訊。

  1. 在 IAPurchaseViewControllerDelegate.swift 裡定義內購完成的 protocol

2. 在 ViewController.swift 定義 Product enum

3. 加入 ViewController.swift 相關邏輯

第 3 行 => 用來存放該使用者所購買的消耗性和非消耗性產品。

第 12~16 行 => 存放使用者購買相關資訊。consumablePurchase 存放消費性產品,nonConsumablePurchase 存放非消費性產品。第一次載入時有初始值,但這並不會是上架 APP 的正確寫法,正確的應該為將使用者購買的資訊存放在遠端的資料庫裡,當應用程式載入時應當是去同步使用者之前所購買過的產品,該範例只是 Demo 用,所以以本文的重點為主。

第 19 行 => 更新當前金幣、可否使用聊天功能。

第 26~30 行 => 更新當前金幣、可否使用聊天功能的邏輯。

第 32~37 行 => 代理 IAPViewController 的協議。

第 41~58 行 => 處理完付款的動作後所觸發的 func,更新當前金幣、可否使用聊天功能。

4. 在 IAPViewController.swift 裡實作取得內購商品的資訊

要取得內購項目的資訊,就得必須透過 Apple 的 StoreKit,並代理 SKProductsRequestDelegate 的協議即可,其實並不會很複雜。

4.1 新增 LodingView.swift 用來告知使用者正在等待 server response

4.2 新增 Custom IAPTableViewCell.swift 用來顯示內購項目的資訊

4.3 在 IAPViewController.swift 加入取得內購項目的邏輯

第 8 行 => 存放產品 ID(Consumable_Product、Not_Consumable_Product)的陣列,該 ID 則為 iTunes Connect 內購項目的 ID。

第 9 行 => 存放 server 回應的產品項目。

第 21~23 行 => 將產品 ID 加入陣列中,以用來請求產品資訊。

第 26 行 => 發送請求取得在 iTunes Connect 內購的產品資訊,並非所有的產品,只會請求有定義的產品 ID。

第 29~32 行 => 當離開該畫面時,移除觀查者,否者該觀查者會一直在背景觀查著 IAP 的交易,如此會耗費效能。

第 37~48 行 => 發送請求以用來取得內購的產品資訊。第 39 行開始請求內購產品,完成後會觸發 productsRequest :didReceive 的 func。

第 84~95 行 => 將取得的產品資訊顯示在畫面上。localizedTitle 顯示「商品名稱」、localizedDescription 顯示「商品描述」,打開 iTunes Connect 對照一下。

第 100~121 行 => 當收到 Server 回應時所會觸發的 func。invalidProductIdentifiers.description 會印出不合法的內購項目,例如:沒有設定價錢、已停用的等等,若是有取得產品資訊則將其存放在 productsArray 裡以用來讓 TableView 顯示。第 106 行則會重新執行 Reload 的動作。

以上都定義好後,試著啟動 APP 看有沒有顯示產品資訊。

備註:請在真機測試。

實作購買消耗性、非消耗性產品、回復購買的動作

上一步驟已取得所有的內購項目並已顯示在畫面上了,接下來的動作就是要將產品的資訊變成一張訂單物件(payment),送到 server 端告知 server 說要購買什麼商品,然後透過遵循 paymentQueue :updatedTransactions 的協議以即可知道交易的結果為何。

  1. 讓 IAPViewController 去代理 SKPaymentTransactionObserver 的協議

除了 SKPaymentTransactionObserver 協議外,其它在上一步驟都已代理了。

2. 在全域變數新增以下變數

3. 實作詢問是否購買或回復的 Action Sheet 的 func

這個 func 的作用在於當使用者點擊產品 cell 時會彈跳出 Action Sheet 供使用者選擇是否購買或回復的動作。

第 4~6 行 => 若是有交易在進行中則直接 return,不可同時有多筆交易進行。

第 17~31 行 => 當使用者點選 Action Sheet 購買所會執行到的 block。第 19 行設定交易流程觀察者,會在背景一直檢查交易的狀態,成功與否會透過 protocol 得知,第 22 行取得使用者所選到的產品,第 25 行則是將產品送到 server 端,成功與為將會透過 paymentQueue :updatedTransactions 來告知我們。不管是消耗性或非消耗性的邏輯都是一樣的。

第 35~45 行 => 當使用者點選 Action Sheet 回復所會執行到的 block。呼叫 restoreCompletedTransactions() 即可執行回復的動作,不過在此要注意的是必須實作 paymentQueueRestoreCompletedTransactionsFinished :_ 否則會有錯誤發生,這在下一步驟會加入。

4. 遵循 SKPaymentTransactionObserver paymentQueue :updatedTransactions 的協議,並可從 transactionState enum 得知交易的結果為何。

第 8~10 => 不管成功與否都要將交易 finish 掉。

第 13 行 => 跟 ViewController(主畫面) 說已完成付款,必須增加金幣。

第 23~35 行 => 交易失敗的原因很多,其中又包含像是輸入 Apple ID 時點選取消等等,從 transaction.error.code 中可以得知相關的錯誤。

第 47 行 => 跟 ViewController(主畫面) 說已復原成功,必須開啟聊天功能。

5. 必須遵循 SKPaymentTransactionObserver 的paymentQueueRestoreCompletedTransactionsFinished :_ 協議,否則在執行回復動作時會有錯誤發生(失敗的可不需實作)。

6. 實作取消、回復購買按鈕的邏輯

以上已完成所有邏輯,試著執行看看,若是有問題請下載完整範例比對看看是不是有哪邊出了問題。

以上範例已放上 GitHub,若有問題歡迎直接在問與答中提問。

--

--