---
# System prepended metadata

title: '[Video Note] [Android] [Billing] Codeible - Selling In-App Products on Android: Implementing Google Play Billing V4'
tags: [Billing, Android, Video Note]

---


### 影片簡介
![image](https://hackmd.io/_uploads/H1qaPmaa1e.png)

- 示範如何使用 Google Play Billing Library 銷售應用內商品
- 涵蓋新增商品、啟動購買流程、後端驗證、確認購買、解鎖商品等流程

---

### 實作 Billing Library 的準備工作
![image](https://hackmd.io/_uploads/Hyc1_76pJl.png)
![image](https://hackmd.io/_uploads/HkYZuXa61x.png)

- 在 `AndroidManifest.xml` 加入網路權限
- 在 `build.gradle` 的 dependencies 中加入 Google Play Billing Library
- 同步專案以套用變更

---

### 測試環境設定
![image](https://hackmd.io/_uploads/rkc3KXpp1l.png)
![image](https://hackmd.io/_uploads/SJSJ5Qap1g.png)
![image](https://hackmd.io/_uploads/Syjx9mTTkx.png)
![image](https://hackmd.io/_uploads/SJk-97p61e.png)
![image](https://hackmd.io/_uploads/ry5-cma6yl.png)
![image](https://hackmd.io/_uploads/H1549766Jx.png)
- 建立已簽署的 App Bundle
- 登入 Google Play Console 並進入「License Testing」新增測試帳號
- 建立新應用，進入「Internal Testing」→「Create New Release」並上傳 App Bundle
- 指定測試帳號，啟動內部測試版本
![image](https://hackmd.io/_uploads/H1zScm6pkl.png)
- 使用支援 Google Play 的模擬器登入測試帳號
![image](https://hackmd.io/_uploads/HJc8976aJg.png)
- 複製測試邀請連結，在模擬器中用 Chrome 開啟並加入測試
![image](https://hackmd.io/_uploads/Sya2cXpTke.png)
![image](https://hackmd.io/_uploads/ryPEKvapye.png)

`https://play.google.com/console/u/0/developers/ `==5131942709901165805== `/app/` ==4972066688623906047== `/tracks/` ==4701385257687167760== `?tab=testers`

---

### 新增應用內商品
![image](https://hackmd.io/_uploads/ryj05QTa1l.png)
![image](https://hackmd.io/_uploads/HyX8Exz0ye.png)
- 在 Play Console 左側點選「Monetize」→「In-app Products」
- 點選「Create Product」，設定商品 ID、名稱、說明與價格
- 儲存並啟用商品
- 重複步驟建立兩個商品作為示範

---

### 顯示商品資訊

- 在 `MainActivity` 宣告 BillingClient 並使用 Builder 模式建立實例
- 呼叫 `enablePendingPurchases()` 與設定 `PurchasesUpdatedListener`
- 建立 `connectToGooglePlayBilling()` 方法，並呼叫 `startConnection()`
- 在 `onBillingSetupFinished` 裡檢查連線狀態，若成功則呼叫 `getProductDetails()`
- `getProductDetails()` 中建立商品 ID 清單並查詢商品資訊
- 使用 `querySkuDetailsAsync()` 查詢商品詳細資料
- 回傳結果後用 `TextView` 顯示商品名稱，`Button` 顯示價格
- 設定 XML 介面元件的 ID 並更新 UI 元件顯示商品資訊
- 確保在 BillingClient 建立後立即呼叫 `connectToGooglePlayBilling()`


---

### 啟動購買流程

- 為價格按鈕加入 `onClickListener`
- 使用 `billingClient.launchBillingFlow` 來開啟購買流程 UI
- 建立 `BillingFlowParams` 並設定 SKU 詳細資料
- 呼叫 `launchBillingFlow` 並傳入 context 與參數

---

### 後端驗證購買

- 所有購買回傳會進入 `onPurchasesUpdated` callback
- 驗證步驟一：確認購買狀態為 PURCHASED 且尚未 acknowledged
- 驗證步驟二：將每筆購買的 token 傳送至後端驗證
- 若 token 從未被使用，代表為有效交易
- 成功驗證後，將購買資訊儲存至資料庫，並回傳訊息給 App

---

### 建立 Firebase Cloud Functions 專案

- 前往 [console.firebase.com](https://console.firebase.com) 建立專案
- 安裝 Node.js 與 Firebase CLI 工具
- 建立資料夾、登入 Firebase 並執行 `firebase init`
- 選擇 Cloud Functions 功能並安裝相依套件

---

### 撰寫後端驗證邏輯

- 在 `functions/index.js` 中建立驗證函數
- 建立 `purchaseInfo` JSON 物件，包含：
  - purchase token
  - order id
  - purchase time
  - isValid（預設為 false）
- 初始化 Firebase Admin 並建立 Firestore 連線
- 驗證是否已存在該 purchase token 的文件
  - 若存在，視為無效購買
  - 若不存在，將 `isValid` 設為 true 並寫入 Firestore
- 最後回傳驗證結果給 App

---

### 部署 Cloud Function 並整合至 App

- 升級 Firebase 計劃至 Blaze（否則無法部署）
- 使用 `firebase deploy` 命令部署函數
- 部署成功後複製函數的 Request URL
- 將 URL 加入 Android App 中以便進行伺服器溝通
- App 呼叫此 URL 並根據伺服器回應判斷是否為有效購買

---

### 使用 Volley 與 Cloud Functions 溝通

- 在 `build.gradle` 中加入 Volley 函式庫
- 建立 `confirmPurchase` 方法並接收 `Purchase` 物件
- 建立請求 URL，包含購買資料（token、時間、訂單 ID）
- 使用 `StringRequest` 建立 POST 請求，傳送至 Cloud Functions
- 建立 Volley 的 `RequestQueue` 並將請求加入佇列
- 在 `onPurchasesUpdated` callback 中呼叫驗證方法
- 至 Firebase Console 將 Firestore 的讀寫規則暫設為 true

---

### 使用 `acknowledgePurchase()` 確認一次性商品

- 預設情況下每筆購買只能購買一次
- 未確認的購買不會付款成功
- 使用 `acknowledgePurchase()` 來確認一次性商品
- 建立 `AcknowledgePurchaseParams` 並設置 purchase token
- 呼叫 `billingClient.acknowledgePurchase()` 並處理回應
- 成功後可在回呼中執行解鎖邏輯，例如顯示 Toast 或解鎖功能

---

### 使用 `consumeAsync()` 處理可重複購買商品

- 更換為可消耗商品（例如：遊戲幣）
- 使用 `consumeAsync()` 來處理購買確認
- 建立 `ConsumeParams` 並設置 purchase token
- 呼叫 `billingClient.consumeAsync()` 並處理回應
- 成功後可執行解鎖或給予物品邏輯，可重複購買

---

### 處理 App 關閉期間的購買行為

- 不應只依賴 `onPurchasesUpdated()`，因為 App 關閉時無法觸發
- 建議在 `onResume()` 中處理未完成確認的購買
- 使用 `billingClient.queryPurchasesAsync()` 查詢已購買項目
- 逐一檢查購買狀態是否為已購買且尚未確認
- 對尚未確認的項目執行購買驗證與確認流程


---
### Terminology
- **Manifest 檔案（Manifest File）**：Android 應用的配置檔，聲明權限與元件。  
- **INTERNET 權限（Internet Permission）**：允許應用存取網路的權限。  
- **build.gradle**：用於配置應用模組與依賴項的 Gradle 檔案。  
- **dependencies 區塊（Dependencies Section）**：定義應用所需的外部函式庫區域。  
- **同步專案（Sync Project）**：將 Gradle 設定應用至專案的過程。  
- **App Bundle**：Android 應用的發行格式，可減少安裝檔案大小。  
- **Google Play Console**：開發者管理 Android 應用的平台。  
- **License Testing**：用於測試應用內購與授權功能的設定區塊。  
- **Internal Testing（內部測試）**：讓指定帳號測試尚未公開的應用版本。  
- **Create Release**：建立應用版本發行流程。  
- **Emulator（模擬器）**：模擬 Android 裝置的虛擬環境。  
- **Android Virtual Device Manager（AVD Manager）**：管理與建立模擬器的工具。  
- **Play Store 圖示（Play Store Icon）**：模擬器支援 Google Play 的識別標誌。  
- **Sign In（登入）**：使用帳號登入 Google Play。  
- **Opt-in 測試（Tester Opt-in）**：測試者接受測試邀請加入測試流程。  
- **Monetize 區塊**：在 Google Play Console 中用於設定商品與營利策略的區塊。  
- **In-app Products（應用內商品）**：可於應用內購買的虛擬項目。  
- **Product ID**：商品的唯一識別碼，創建後無法更改。  
- **Set Price**：設定商品價格的操作。  
- **Activate（啟用）**：使商品在 Play 商店中上架生效。  
- **MainActivity**：應用的主要邏輯控制類別。  
- **BillingClient 變數**：管理 Google Play 購買流程的類別實例。  
- **Builder 方法**：建構 BillingClient 物件所使用的設計模式方法。  
- **enablePendingPurchases()**：啟用對暫掛交易的支援，是必須呼叫的方法。  
- **PurchasesUpdatedListener**：接收購買更新事件的監聽器。  
- **connectToGooglePlayBilling()**：建立與 Google Play 的連線方法。  
- **startConnection()**：開始連線至 Billing Service 的方法。  
- **BillingClientStateListener**：回報 BillingClient 連線狀態的監聽器。  
- **onBillingServiceDisconnected()**：當服務中斷時的回呼方法。  
- **onBillingSetupFinished()**：設定完成時的回呼方法。  
- **BillingResult**：表示購買作業結果的物件。  
- **querySkuDetailsAsync()**：非同步查詢 SKU 詳細資料的方法。  
- **SkuDetailsParams**：查詢商品所需參數的資料結構。  
- **setSkusList()**：設定要查詢的商品 ID 清單的方法。  
- **setType()**：設定商品類型，如消費型或訂閱型。  
- **SkuDetailsResponseListener**：取得 SKU 資訊查詢結果的回呼介面。  
- **RecyclerView**：顯示大量資料的 UI 元件。  
- **CardView**：以卡片方式呈現資料的 UI 元件。  
- **TextView**：用來顯示文字的 UI 元件。  
- **Button**：用來顯示或觸發操作的按鈕 UI 元件。  
- **setText()**：設定 TextView 或 Button 顯示的文字方法。  
- **getTitle()**：從 SkuDetails 取得商品標題的方法。  
- **getPrice()**：從 SkuDetails 取得商品價格的方法。  
- **findViewById()**：透過元件 ID 取得 UI 元件的函式。  
- **Activity XML Layout**：定義 UI 結構的 XML 檔案。  
- **ConstraintLayout**：可透過約束定義元件位置的佈局容器。  
- **ID 命名（View ID）**：設定 UI 元件的唯一識別名稱。  
- **TextView ID：item_name**：顯示商品名稱的元件 ID。  
- **Button ID：item_price**：顯示商品價格的按鈕 ID。  
- **run App（執行應用）**：啟動應用並觀察 UI 效果。  
- **SKU（Stock Keeping Unit）**：商品的識別碼，一般代表單一可銷售項目。  
- **暫掛交易（Pending Purchase）**：尚未完成或處理中的購買交易。  
- **UI 顯示（UI Display）**：在應用畫面中顯示商品資訊的方式。  
- **查詢商品細節（Get Product Details）**：從 Play Console 取得商品的詳細資料。  
- **List 物件**：儲存多筆資料的集合型別，常用於 SKU ID 清單。  
- **Builder 設計模式**：用來建構複雜物件的一種程式設計方法。  
- **Product 資訊展示（Product Presentation）**：將產品資訊呈現在使用者介面中的作法。
- **OnClickListener**：用於監聽按鈕點擊事件的介面。  
- **launchBillingFlow()**：啟動 Google Play 的購買流程 UI 方法。  
- **BillingFlowParams**：用來設定購買流程參數的物件。  
- **setSkuDetails()**：為購買流程設定商品資訊的方法。  
- **onPurchasesUpdated()**：接收購買完成後結果的回呼方法。  
- **purchase list**：儲存使用者已購買項目的清單物件。  
- **response code**：BillingResult 中代表購買結果狀態的代碼。  
- **Purchase.getPurchaseState()**：取得購買狀態的方法。  
- **Purchase.isAcknowledged()**：檢查購買是否已確認的方法。  
- **purchase token**：唯一識別單一交易的字串，用於驗證。  
- **後端伺服器（Backend Server）**：應用用來處理邏輯與儲存資料的後端系統。  
- **Firebase Cloud Functions**：Firebase 提供的無伺服器後端服務平台。  
- **Firestore Database**：Firebase 提供的即時 NoSQL 資料庫。  
- **firebase console**：管理 Firebase 專案的網頁後台介面。  
- **firebase tools**：Firebase CLI 工具，用於本機管理與部署功能。  
- **npm install -g firebase-tools**：安裝 Firebase CLI 的命令。  
- **firebase init**：初始化 Firebase 專案與功能的命令。  
- **functions/index.js**：定義雲端函式邏輯的主要檔案。  
- **exports.functionName**：定義並匯出一個 Firebase 雲端函式的方法。  
- **request handler function**：處理傳入 HTTP 請求的函式。  
- **purchaseInfo（JSON）**：儲存購買資訊的資料物件。  
- **orderId**：Google Play 提供的訂單編號。  
- **purchaseTime**：商品購買時間。  
- **isValid**：標記購買是否有效的布林值欄位。  
- **req.query**：從請求網址查詢參數中取得資料的方式。  
- **admin module**：Firebase Admin SDK，用來存取 Firestore 與其他服務。  
- **admin.initializeApp()**：初始化 Firebase Admin SDK 的方法。  
- **admin.firestore()**：取得 Firestore 實例的方法。  
- **doc()**：存取 Firestore 文件的函式。  
- **get()**：讀取指定文件資料的方法。  
- **exists**：判斷文件是否存在的屬性。  
- **set()**：將資料寫入指定文件的方法。  
- **then()**：處理非同步操作完成後的回呼方法。  
- **firebase deploy --only functions**：將雲端函式部署至 Firebase 的命令。  
- **Blaze Plan**：Firebase 提供的按用量付費方案，必要條件以啟用部署功能。  
- **request URL**：Firebase 雲端函式的存取網址，用於前後端溝通。  
- **HTTP 請求（HTTP Request）**：應用與伺服器之間資料傳遞的方式。  
- **非同步通訊（Asynchronous Communication）**：在不阻塞主執行緒下進行的伺服器互動。  
- **資料驗證（Data Validation）**：確認資料真實性與一致性的過程。  
- **token 重複檢查（Duplicate Token Check）**：驗證是否為未使用過的交易憑證。  
- **回應通知（Response Notification）**：伺服器將驗證結果回傳給前端應用的動作。  
- **JSON 物件（JSON Object）**：JavaScript 的資料格式，用來傳輸結構化資料。  
- **資料結構（Data Structure）**：資料儲存與組織方式。  
- **雲端部署（Cloud Deployment）**：將程式碼上傳至雲端平台以供執行的動作。  
- **伺服器端邏輯（Server-side Logic）**：於伺服器中處理業務邏輯的程式碼。  
- **驗證流程（Verification Flow）**：檢查交易合法性的整體程序。  
- **HTTP GET 方法**：從伺服器讀取資料的標準請求方法。  
- **Node.js**：JavaScript 執行環境，常用於撰寫 Firebase 函式。  
- **request.query.purchaseToken**：從 HTTP 請求中取得購買憑證的方式。  
- **雲端函式端點（Cloud Function Endpoint）**：供應用端呼叫的公開函式位址。  
- **程式碼編輯器（Code Editor）**：撰寫與編輯程式碼的工具，如 VS Code。  
- **終端機（Terminal）**：輸入命令與執行程式的工具介面。  
- **資料回傳（Data Response）**：伺服器將資料發送回應用的過程。  
- **資料查詢（Data Query）**：讀取資料庫中特定資料的操作。  
- **集合（Collection）**：Firestore 中用來儲存多個文件的容器。  
- **文件 ID（Document ID）**：Firestore 中識別單一文件的鍵值。  
- **布林值（Boolean Value）**：只允許 true 或 false 的資料型別。
- **Volley**：一個 Android 網路通訊函式庫，用來執行 HTTP 請求。  
- **StringRequest**：Volley 提供的類別，用來建立字串型態的 HTTP 請求。  
- **POST 請求（POST Request）**：用來向伺服器傳送資料的 HTTP 方法。  
- **RequestQueue**：Volley 的請求佇列，負責排程與執行網路請求。  
- **Response.Listener**：收到伺服器回應時執行的回呼函式。  
- **Response.ErrorListener**：請求失敗或錯誤時執行的回呼函式。  
- **URL 查詢參數（Query Parameter）**：URL 中用來傳遞資料的鍵值對格式。  
- **Key-Value Pair**：一種資料格式，由鍵（Key）與對應的值（Value）組成。  
- **Ampersand（&）**：分隔多組查詢參數的符號。  
- **Request URL**：發送 HTTP 請求的目標網路位置。  
- **Firebase Console Firestore 規則（Firestore Rules）**：控制資料庫讀寫權限的安全規則。  
- **acknowledgePurchase()**：Google Play Billing 中用來確認購買的 API 方法。  
- **one-time purchase**：一次性購買的商品類型。  
- **consumeAsync()**：用於消耗可重複購買商品的 API 方法。  
- **AcknowledgePurchaseParams**：用於建立 acknowledgePurchase 請求參數的類別。  
- **setPurchaseToken()**：設置要確認或消耗的購買憑證的方法。  
- **AcknowledgePurchaseResponseListener**：處理 acknowledge 回應的監聽器。  
- **Toast Message**：Android 中用來顯示暫時通知的 UI 元件。  
- **“You already own this item” 錯誤**：表示尚未確認或消耗的商品無法再次購買。  
- **ConsumeParams**：用於建立 consumeAsync 請求參數的類別。  
- **ConsumeResponseListener**：處理 consumeAsync 回應的監聽器。  
- **Multiple Purchase（多次購買）**：允許使用者多次購買相同商品的功能。  
- **onResume() 方法**：當應用重新啟動時呼叫的 Activity 生命周期方法。  
- **queryPurchasesAsync()**：用來查詢已購買商品的非同步方法。  
- **ProductType.INAPP**：代表查詢的商品為應用內一次性商品。  
- **PurchasesResponseListener**：回傳購買清單的回呼介面。  
- **BillingResult.getResponseCode()**：取得請求回應碼的方法。  
- **Purchase.PurchaseState.PURCHASED**：代表購買狀態為已購買的常數。  
- **未確認購買（Unacknowledged Purchase）**：尚未呼叫 acknowledge 的購買交易。  
- **購買驗證流程（Purchase Verification Flow）**：確認交易有效性的整體步驟。  
- **背景交易處理（Background Purchase Handling）**：應用未開啟時處理購買的機制。  
- **主動回查（Active Polling）**：應用主動查詢伺服器狀態的操作方式。  
- **Firebase Blaze 計畫（Blaze Plan）**：Firebase 按用量計費方案，支援雲端部署。  
- **Firebase Functions 部署（Function Deploy）**：將後端功能上傳到雲端執行的操作。  
- **JSON Response 解析**：從伺服器回傳的 JSON 資料中解析所需欄位。  
- **isValid 屬性**：代表此購買資料是否有效的欄位。  
- **App Lifecycle（應用生命周期）**：Android 應用的啟動、暫停、恢復等階段流程。  
- **Context 參數**：Android 中代表目前運行環境的物件，必要於多種 API 呼叫。  
- **Builder 模式**：用於構建複雜參數物件的設計模式。  
- **非同步回呼（Async Callback）**：操作完成後由系統通知的執行方法。  
- **Firestore Collection 路徑**：儲存資料的集合在資料庫中的位置描述。  
- **Firebase Admin SDK**：Firebase 用來存取雲端資源的後端 SDK。  
- **Document ID（文件 ID）**：Firestore 中每筆資料的唯一識別碼。  
- **Firebase CLI（命令列工具）**：本機與 Firebase 雲端進行互動的工具。  
- **Node.js 環境**：執行 Firebase Cloud Functions 所需的 JavaScript 執行環境。  
- **HTTP 請求錯誤處理（HTTP Error Handling）**：處理網路錯誤狀態的程式邏輯。  
- **初始化 Firebase 專案（Firebase Init）**：建立雲端功能初始設定的流程。  
- **Firebase Hosting**：Firebase 提供的靜態網站托管服務，與 Functions 搭配使用。  
- **Key-Value URL 編碼（URL Encoding）**：將資料格式轉為瀏覽器可識別的查詢參數。  
- **Firebase Function Endpoint 呼叫**：透過網路發送資料至 Firebase 雲端函式的方式。