Android 實作 IAP 內購式商品 (Selling In-app Products)(2/2)

In-app Billing

本篇文章使用 Version 3 API。本篇為實戰篇,概念篇請看 In-app Billing(App內付款) 概念

準備要使用 In-app Billing 的應用程式

下載 Library

打開 Android SDK Manager,拉到最下方的 Extras,找到 Google Play Billing Library 勾選後安裝。之後就會在你的 Android SDK 目錄中找到該 Library,位置如下:
/{你的SDK目錄}/extras/google/play_billing/in-app-billing-v03

在開發者控制台新增 App

當你在開發者控制台新增一個 App 時,會產生一個公開金鑰,只有在第一次建立 App 時才需要產生這個金鑰,之後會用在你的 App 裡,用來和 Google Play 通訊。

執行步驟:
  1. 登入開發者控制台,你必須同時擁有 Google Checkout 帳戶。
  2. 點選"體驗新版介面"進入新的開發者控制台。
  3. 在"所有應用程式"頁面中,
    1. 點選"新增應用程式"
    2. 輸入應用程式名稱
    3. 點選"準備商店資訊"
  4. 在"服務和API"頁面,記下由 Google Play 產生的 public license key,必須在應用程式中加入這組鑰匙。

在專案中加入 In-app Billing Library

要使用 Version 3 API 必須在專案中加入 IInAppBillingService.aidl 檔,透過它來和 Google Play 服務溝通。這個檔案可以在之前下載的 Library 中找到。以下選擇一種情況來使用,是新專案或在已有專案中使用。
新專案的執行步驟:
  1. 在 Eclipse 中 Import -> Android -> Existing Android Code Into Workspace,找到 Library 目錄中的 in-app-billing-v03/samples 目錄之下的 TrivialDrive,將它滙入 Eclipse 專案中。在該專案按右鍵,Refactor -> Rename 改成你的專案名稱。
  2. 打開 AndroidManifest.xml 檔,修改 Package 屬性的值為你自己的專案。
  3. 接下要修正程式碼中的 Import 部份。
  4. 將在開發者控制台取得的 Public Key 加到 MainActivity.java 中。
現有專案的執行步驟:
  1. 複製 IInAppBillingService.aidl 檔案到你的專案(在Eclipse 中 Import 到 /src 目錄)。
  2. 建置(Buile)你的專案,應該會在 /gen 目錄中看到自動產生的 IInAppBillingService.java。
  3. 在專案中加入 Sample 專案中 /util 目錄下的所有檔案,記得修改 package 名稱。

設定權限

在 AndroidManifest.xml 中的 permission 加入:
<uses-permission android:name="com.android.vending.BILLING" />

連結到 Google Play

你必須綁定 (bind) Activity 到 Google Play 的 In-app Billing 服務,利用它來送出請求給 Google Play。在範例檔中提供了數個簡便的類別,去掌控綁定到 In-app Billing 服務的任務,所以你不必去處理網路連線的問題。

在 onCreate 中建立一個 IabHelper 物件,用來設定和 Google Play 的同步通訊。
安全性建議:強烈建議不要直接把 Public Key 寫在程式碼中,而應該在執行時期從加密過的字串中取得。這可避免被惡意的修改APK檔中的 Public Key 。
之前在開發者控制台取得 Public Key 就是被編碼過的金鑰,利用 IabHelper 來取得還原後的 Public Key,程式碼如下:
IabHelper mHelper;

@Override
public void onCreate(Bundle savedInstanceState) {
   // ...
   String base64EncodedPublicKey = "被編碼過的 PUBLIC_KEY";

   // compute your public key and store it in base64EncodedPublicKey
   mHelper = new IabHelper(this, base64EncodedPublicKey);
}
接下來處理服務的綁定。呼叫 IabHelper 的 startSetup 方法,因為設定是非同步的,所以要建立一個設定完成的偵聽器,當 IabHelper 設定完成後,該偵聽器會被呼叫。在設定的同時,會一併檢查是否支援 In-app Billing API Version 3,程式碼如下:
mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
   public void onIabSetupFinished(IabResult result) {
      if (!result.isSuccess()) {
         // Oh noes, there was a problem.
         Log.d(TAG, "Problem setting up In-app Billing: " + result);
      }            
         // Hooray, IAB is fully set up!  
   }
});
如果設定成功,就可使用 mHelper(IabHelper) 和 Google Play 服務做溝通。可以在 App 啟動時去查詢使用者擁有哪些內購式商品,然後記得在結束 Activity 時,要去釋放 mHelper ,如下:
@Override
public void onDestroy() {
   if (mHelper != null) mHelper.dispose();
   mHelper = null;
}

建立可以讓 App 內購的商品

在開發者控制台建立可被購買的數位商品,步驟如下:
  1. 建立一個簽名(signed)過的APK檔,也就可以發佈到 Google Play 的應用程式。
  2. 在開發者控制台打開你早先建立的應用程式。
  3. 切換到 APK 頁面,然後上傳簽名過的 APK 檔,BUT!暫時不要發佈。
  4. 切換到"應用程式內產品"頁面。
  5. 點選"新增產品",選擇"商品類型",輸入產品 ID,這個 ID 就是 SKU,是個唯一值不能重覆。
  6. 完成產品資訊的填寫並儲存後,將它啟用。現在你的 App 就可以購買該商品了。

查詢可購買的商品

你可以在程式碼中撰寫查詢和這個 App 相關的商品資訊,包含產品名稱、類型、說明和價格,這可讓你列出使用者尚未擁有並可購買的商品清單。
當要執行查詢時,必須明確指定產品ID,可以在開發者控制台的"應用程式內產品"頁面看到產品清單,在"名稱/ID"這個欄位即為該產品的ID。
要取得產品資料可以使用 mHelper(IabHelper) 物件的 queryInventoryAsync(boolean, List, QueryInventoryFinishedListener) 方法。
  • 參數1:是否可以檢索產品資料
  • 參數2:想要查詢的一個或多個產品的ID
  • 參數3:偵聽查詢完成事件
程式碼如下:
List additionalSkuList = new List();
additionalSkuList.add(SKU_APPLE);
additionalSkuList.add(SKU_BANANA);
mHelper.queryInventoryAsync(true, additionalSkuList, mQueryFinishedListener);
如果查詢成功,結果會儲存在 Inventory 物件中回傳給偵聽器。以下程式碼展示如何從結果中取得產品的價格:
IabHelper.QueryInventoryFinishedListener mQueryFinishedListener = new IabHelper.QueryInventoryFinishedListener() {
   public void onQueryInventoryFinished(IabResult result, Inventory inventory)   
   {
      if (result.isFailure()) {
         // handle error
         return;
       }

       String applePrice =
          inventory.getSkuDetails(SKU_APPLE).getPrice();
       String bananaPrice =
          inventory.getSkuDetails(SKU_BANANA).getPrice();

       // update the UI 
   }
}

購買內購式商品

要讓使用者購買某項內購式商品,要在程式內呼叫 mHelper(IabHelper) 物件的 launchPurchaseFlow(Activity, String, int, OnIabPurchaseFinishedListener, String) 方法,必須在 Activity 的主執行緒中呼叫,該方法的參數說明:
  • 參數1:目前這個 Activity 本身。
  • 參數2:產品ID(SKU)。
  • 參數3:請求碼,任意正整數。Google Play 會回傳這個請求碼給 Activity 的 onActivityResult 。
  • 參數4:偵聽完成事件。
  • 參數5:用來附加其他資訊的字串,可以是空的。好的做法是,傳送一組字串,讓應用程式可以識別這個購買商品的使用者。
使用範例碼:
mHelper.launchPurchaseFlow(this, SKU_GAS, 10001, mPurchaseFinishedListener, "bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ");
如果購買成功,Google Play 會將資料儲存在 Purchase 物件中回傳,然後偵聽就會收到,程式碼如下:
IabHelper.OnIabPurchaseFinishedListener mPurchaseFinishedListener = new IabHelper.OnIabPurchaseFinishedListener() {
   public void onIabPurchaseFinished(IabResult result, Purchase purchase) 
   {
      if (result.isFailure()) {
         Log.d(TAG, "Error purchasing: " + result);
         return;
      }      
      else if (purchase.getSku().equals(SKU_GAS)) {
         // consume the gas and update the UI
      }
      else if (purchase.getSku().equals(SKU_PREMIUM)) {
         // give user access to premium content and update the UI
      }
   }
};
為了安全性,建議去檢查回傳的附加字串(參數5)是否相符。

查詢已購買產品

當整個購買流程結束後,購買的資料會被本地端快取,讓應用程式可以頻繁的查詢,例如 App 啟動時,或 resume 時。

查詢時一樣呼叫 mHelper(IabHelper) 物件的方法 queryInventoryAsync(QueryInventoryFinishedListener),參數是偵聽完成事件的偵聽器。程式碼如下:
mHelper.queryInventoryAsync(mGotInventoryListener);

IabHelper.QueryInventoryFinishedListener mGotInventoryListener = new IabHelper.QueryInventoryFinishedListener() {
   public void onQueryInventoryFinished(IabResult result,
      Inventory inventory) {

      if (result.isFailure()) {
        // handle error here
      }
      else {
        // does the user have the premium upgrade?
        mIsPremium = inventory.hasPurchase(SKU_PREMIUM);        
        // update UI accordingly
      }
   }
};

測試應用程式

因為 Google 不允許使用開發者帳號直接購買內購式商品,所以開發者必須在原本的帳號之下建立一個測試帳號,如此才能購買已上傳但未發佈的 App。

在開發者控制台新增一個(或多個)測試帳號。進入開發者控制台->設定->帳戶詳細資料,找到"授權測試"項目,新增測試用 Gmail 帳戶。

透過 adb 的方式安裝已簽署過的 APK 檔(此APK是已經上傳到開發者控制台的那個)。這裡要注意的是,裝置的 Android 版本必須是 2.2 版 (level 8) 以上,裝置中的 Google Play App 必須是 3.9.16 版以上。

要安裝 APK 到裝置使用以下指令:
adb install apk檔路徑
建議讓環境單純,關掉所有模擬器,只接一台裝置到 usb,使用 adb devices 是否有查到一台裝置,然後再使用上面的指令。

Google 有限制可購買商品的帳號不能是開發者帳號,也就是說如果你用你的開發者帳號去測試購買商品時,會出現"內容發佈者無法購買這項商品"的訊息,你必須使用測試帳號來登入你的裝置,才能用這個測試帳號來購買。我自己的實驗是,手機無解,因為手機不能多帳戶切換,最不麻煩的做法是剛好你朋友有 Android 手機,把它加為測試帳戶,利用它的裝置來測這樣最容易,但他的帳號必須有設定信用卡資料。

然後很重要的事,當你用測試帳戶購買商品成功後,記得在開發者控制台將此次的購買退款,否則這些購買行為都會成立,是要真的付錢的,如果你用朋友的帳戶測試,他應該會殺了你 XD。

參考資料: Selling In-app Products
本文網址:https://blog.tonycube.com/2013/05/android-iap-selling-in-app-products22.html
Tony Blog 撰寫,請勿全文複製,轉載時請註明出處及連結,謝謝 😀

20 則留言

  1. 請問是不是要先上傳一個空的apk 裡面有含"com.android.vending.BILLING" 權限
    才可以在 後台的 "應用程式內產品" 上新增 IAB 的產品
    謝謝

    回覆刪除
    回覆
    1. 是的,記得不要選到"發佈",要儲存為"草稿",不然空的APK就會出現在商店了。

      刪除
    2. 作者已經移除這則留言。

      刪除
    3. 目前碰到 "這一版的應用程式未提供google play結帳功能"
      不知道你有沒有碰過

      刪除
    4. 這裡有人遇到相同的問題
      http://lp43.blogspot.tw/2012/04/google-play-in-app-billing.html
      可以參考看看

      刪除
  2. 請問一下~我現在是用這個V3去開發一個AP,我在使用者後端開發者平板設定商店導到電子錢包的網頁,在設定新增完我的帳戶資料後(原來要輸入英文的姓名?),還有帳戶資訊後,我按點選取後就不能修改,我看該頁的說明是說必須等候三天的銀行驗證,真的必須等到三天才可以驗證成功嗎?是否要等到驗證成功才可以看到消費者購買AP的紀錄?我這邊有先測試帳戶購買我的AP,可是我找不到任何消費紀錄可以讓我退款,想大大是否知道?感謝您的幫忙~

    回覆刪除
    回覆
    1. 電子錢包的部份其實是另外一個流程,不能含到購買流程中。

      意思是,使用者要先申請電子錢包,這部份確實必須等驗證,但我當初申請時是Google Checkout,後來才變成Google Wallet,忘了有沒有等到三天。

      之後才會是一般的付費購買流程,所以你必須等電子錢包那個認證流程完成後,該帳號才有購買的能力。
      之後的任何購買行為就不再需要電子錢包認證的程序,而只有購買的程序。

      在開發者後台,一定可以看見消費紀錄,如果沒看到,表示沒有購買成功。

      刪除
    2. "記得在開發者控制台將此次的購買退款"
      請問一下, 在"開發者控制台"頁面中,我找不到可以點選退款的相關功能耶~
      可否指點一下呢??(或是已經移到其他頁ˇ面了??)

      感謝阿

      刪除
    3. https://support.google.com/googleplay/android-developer/answer/137997
      使用者必須取消購買(移除App),你才能退款。

      刪除
    4. 感謝tony大大回應~
      經過與google確認, 我的後台開發帳號有bug:
      =====
      Hi Miss Syu,
      Thank you for reaching out to us.Support via email is only available in English at the moment. Please respond in English if possible. We look forward to offering our support services in additional languages soon.

      I understand your payment settings is blank. Our records indicate that your account was made in the old platform which is causing the blank page and incomplete settings. I've requested our specialists to move your account to the new platform. I’ll respond with additional information as soon as possible.

      In the meantime, feel free to contact us again if you have further questions. We appreciate your patience.

      Sincerely,

      Cristina
      The Google Wallet for Business Team

      If you have additional questions, please visit our Help Center at https://support.google.com/payments/

      If you received this communication by mistake, please don't forward it to anyone else (it may contain confidential or privileged information), please erase all copies of it, including all attachments, and please let the sender know it went to the wrong person. Thanks.
      =================


      大概原因就是: 他們沒有把我的帳號 轉移到 新的系統 ,導致我看不到"委刊單"列表!!!

      提供給大家參考~~說不定也有人也會遇到 XDXD

      刪除
    5. 讚~~感謝你的分享 :)

      刪除
  3. 您好,
    我碰到的問題是新創的包名、商品前幾天都可以測試購買,
    有一陣子沒買後來再使用,
    就會出現找不到您要購買的商品,
    程式碼完全沒有任何變動,
    請問這是什麼原因造成的,
    大大知道嗎?

    回覆刪除
    回覆
    1. 這個我也不知道。你有更新過開發工具ADT嗎?
      有時候更新版本後相同的程式碼也會出問題,
      大部份是在 library 的設定,但我不知道你這個是怎麼發生的,
      無能為力~~~

      刪除
    2. 我也認為很奇怪啊,
      沒有更新過ADT,
      新的包名、商品就可以買,
      我買了五十幾次都可以,
      可是幾天後又出現找不到此項商品,
      我去檢查請求的商品ID也確認商店中確實有此商品,
      key也沒有錯,
      也是使用export的包,
      也使用測試帳號,
      各種都檢查過了,
      就是出現這個錯誤,
      完全無解啊!

      刪除
  4. 請問一下~~
    我已經上傳APK和在"應用程式內產品"新增商品了
    可是我在查詢商品時,呼叫onQueryInventoryFinished
    它都回傳Inventory = null給我
    請問這是我哪邊設定有誤嗎??

    回覆刪除
    回覆
    1. 我也不太清楚。
      如果 result.isFailure() 沒有 true,那應該是有連上線才對,理論上 Inventory 會是有東西的。這個要在查查看。

      刪除
    2. 好像過一段時間就好了@@
      現在可以撈取商品資訊了

      刪除
    3. 是要等一段時間,和上傳 apk 一樣,不會立即可用。
      繼續加油吧~~

      刪除

留言小提醒:
1.回覆時間通常在晚上,如果太忙可能要等幾天。
2.請先瀏覽一下其他人的留言,也許有人問過同樣的問題。
3.程式碼請先將它編碼後再貼上。(線上編碼:http://bit.ly/1DL6yog)
4.文字請加上標點符號及斷行,難以閱讀者恕難回覆。
5.感謝您的留言,您的問題也可能幫助到其他有相同問題的人。