Androidアプリ定期購入(月額課金など)について
2016年 12月 21日
E・レシピ担当の長谷川です。以前にも「Apache Solrによる検索サジェスト機能の実現」というエンジニアブログを執筆しました。
11月末にE・レシピのプレミアムメンバー機能を実装したAndroidアプリをリリースしました。こちらは既にリリースされているアプリに課金機能を追加した形になります。そこで本日はAndroidアプリの定期購入(the In-app Billing Version 3 API)について書きたいと思います。内容が不正確である可能性もありますので、公式ページをご参考にして実装を行ってください。

課金処理のフロー
まずアプリで購入情報を行い、サーバーでレシートの検証を行います。

- Google Playにユーザーのアプリ内購入情報を照会し、購入リクエスト送信
- developerPayloadのチェック
- 購入情報をサーバーへ送信
- 署名検証
- Google Play Developer APIを使って購入の詳細情報を取得
- 課金処理(会員登録など)
- アプリ内課金処理(有効期限登録など)
商品リスト作成
アプリ内課金実装
AIDLファイルをプロジェクトに追加
まず、「SDK Manager」で「Google Play Billing Library」をダウンロードをします。そして下記から「In-app Billing Version 3」サービスへのインターフェイスを定義する AIDLファイルを取得します。
{ANDROID_SDK_PATH}/extras/google/play_billing/IInAppBillingService.aidl
取得した「IInAppBillingService.aidl」を下記にコピーします。
{PROJECT_PATH}/app/src/main/com/android/vending/billing/IInAppBillingService.aidl
最後にアプリをビルドします。プロジェクト内の「/gen」ディレクトリに「IInAppBillingService.java」が生成されれば完了です。
ヘルパークラス追加
「Android SDK」で提供されているサンプルアプリの下記パスにあるヘルパークラスを課金実装を行うプロジェクトにコピーします。このヘルパークラスを利用してアプリ内課金を実装していきます。このような手順にはあまり馴染みがなかったため驚きましたが、公式ページにもきちんと記載がされていました。
{SAMPLE_PROJECT_PATH}/app/src/main/java/com/example/android/trivialdrivesample/util
AndroidManifest更新
アプリが適切なパーミッションをリクエストする必要があるので、アプリ内課金パーミッションを宣言します。
<uses-permission android:name="com.android.vending.BILLING" />
IabHelperセットアップ
「Google Play」とのコネクションを繋ぐために、「IabHelper」をセットアップします。「base64EncodedPublicKey」は「Google Play Developer Console」 -> 実装アプリ -> サービスとAPI -> このアプリのライセンスキーから取得することができます。
IabHelper mHelper;
@Override
public void onCreate(Bundle savedInstanceState) {
/* アプリのライセンスキー */
String base64EncodedPublicKey;
mHelper = new IabHelper(this, base64EncodedPublicKey);
mHelper.enableDebugLogging(BuildConfig.DEBUG);
/* IabHelper初期化 */
mHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
public void onIabSetupFinished(IabResult result) {
if (!result.isSuccess()) {
/* セットアップ失敗処理 */
return;
}
if (mHelper == null) {
return;
}
/* セットアップ完了 */
/* セットアップ完了後処理 */
}
});
}
Activity終了時には必ずアプリ内課金サービスのバインドを解除するようにします。
@Override
public void onDestroy() {
super.onDestroy();
if (mHelper != null) {
mHelper.disposeWhenFinished();
}
mHelper = null;
}
ベータ(アルファ)版テスト公開
購入情報取得
購入情報を取得します。リスナーを設定し、処理が完了次第そのリスナーが呼ばれます。「SKU_PREMIUM」の部分には事前に作成したアイテムIDを指定します。購入している場合には「purchase」に購入情報が入ってきます。この購入情報を元にUIのアップデートを行うなどの処理を行います。
try {
mHelper.queryInventoryAsync(mGotInventoryListener);
} catch (IabAsyncInProgressException e) {
/* 購入情報取得失敗処理 */
}
/* 購入情報取得完了処理 */
IabHelper.QueryInventoryFinishedListener mGotInventoryListener = new IabHelper.QueryInventoryFinishedListener() {
public void onQueryInventoryFinished(IabResult result, Inventory inventory) {
if (mHelper == null) {
return;
}
if (result.isFailure()) {
/* 購入情報取得失敗処理 */
return;
}
Purchase purchase = inventory.getPurchase(SKU_PREMIUM);
if (purchase == null) {
/* 未購入状態処理 */
return;
}
/* 購入状態処理 */
}
};
購入
購入処理も同様にヘルパークラスを利用して課金処理を行い、セットしたリスナー内で、結果を取得し、購入処理を行います。「REQUEST_PURCHASE」にはリクエストコードを、「payload」には「Google Play」から返る結果に埋め込まれる値を設定します。この値は開発者が自由に設定できますが、しばしばユーザ−IDを設定することが多く、購入者の判別に利用できます。
try {
mHelper.launchSubscriptionPurchaseFlow(
this,
SKU_PREMIUM,
REQUEST_PURCHASE,
mPurchaseFinishedListener,
payload
);
} catch (IabAsyncInProgressException e) {
/* 購入失敗処理 */
}
/* 課金完了処理 */
IabHelper.OnIabPurchaseFinishedListener mPurchaseFinishedListener = new IabHelper.OnIabPurchaseFinishedListener() {
public void onIabPurchaseFinished(IabResult result, Purchase purchase) {
if (mHelper == null) {
return;
}
if (result.isFailure()) {
/* 課金失敗処理 */
return;
}
/*
* developerPayloadのチェック
* 購入時に設定した「payload」と比較することで、購入者チェックをすることができます
*/
if (purchase.getDeveloperPayload() != payload) {
/* 課金無効処理 */
return;
}
/* アイテムID毎に処理を分けることができます */
if (purchase.getSku().equals(SKU_PREMIUM)) {
/* 購入処理 */
}
}
};
購入結果を受け取るために、「onActivityResult」をオーバーライドして、下記のように記述する必要があります。このコードがないと結果を受け取ることができません。初めこの処理を忘れていて、コールバックが呼ばれない現象が起こり混乱しました。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (mHelper == null) {
return;
}
if (!mHelper.handleActivityResult(requestCode, resultCode, data)) {
super.onActivityResult(requestCode, resultCode, data);
}
}
署名検証
「purchase」から下記のような購入情報を取得することができます。
mItemType | アプリ内アイテムの場合 : inapp 定期購入の場合 : subs |
---|---|
mOrderId | 注文識別子(テスト購入場合は空欄) |
mPackageName | アプリケーションパッケージ |
mSku | アイテムの商品識別子 |
mPurchaseTime | 商品が購入された時間(1970年1月1日から始まるミリ秒) |
mPurchaseState | 購入状況(0 : 購入済み 1 : キャンセル済み 2 : 返金済み) |
mDeveloperPayload | 開発者が指定する文字列 |
mToken | 任意のアイテムとユーザーのペアにおける購入を一意に識別するトークン |
mOriginalJson | 注文の詳細情報を含むJSON形式の文字列 |
mSignature | 秘密鍵で署名された購入データの署名を含む文字列 |
mIsAutoRenewing | 定期購入が自動更新されたかどうか |
「レシート(mOriginalJson)」「署名(mSignature)」「ユーザーID」をサーバーに送り、署名検証を行います。Base64エンコードのRSA公開鍵であるアプリのライセンスキーからPEM形式の証明書を生成し、これを「openssl_get_publickey」に渡せば、PHPで公開鍵を生成することができます。そして「レシート」のSHA1ハッシュ値と「公開鍵」で復号した「署名」を使用して、検証を行います。
/**
* 署名検証
*
* @param string $receipt レシート
* @param string $signature 署名
*
* @return boolean 署名検証結果
*/
private function verifySignature($receipt, $signature)
{
/* GooglePlayのアプリの公開鍵をPEM形式に変換したもの */
$publicKey = openssl_get_publickey($this->pemPublicKey());
/* POSTでデータを渡すと、「+」が「 」に置き換わっているため置換 */
$receipt = $this->replaceSpaceForPostData($receipt);
$decodedSignature = base64_decode($this->replaceSpaceForPostData($signature));
$result = (int)openssl_verify($receipt, $decodedSignature, $publicKey);
openssl_free_key($publicKey);
if ($result <= 0) {
return false;
}
return true;
}
/**
* RSA(PEM形式)公開鍵の取得
*
* @return string RSA(PEM形式)公開鍵
*/
private function pemPublicKey()
{
return "-----BEGIN PUBLIC KEY-----" . PHP_EOL .
chunk_split(PUBLIC_KEY, BASE64, PHP_EOL) .
"-----END PUBLIC KEY-----";
}
/**
* POSTデータのスペース置換
*
* @param string $data POSTデータ
*
* @return string POSTデータ
*/
private function replaceSpaceForPostData($data)
{
return str_replace(' ', '+', $data);
}
有効期限取得
Google Play Developer APIを使って、定期購入の有効期限を取得します。ライブラリを使用することもできますので、必要に応じて使用してください。「API」と「アプリ」の連携を行い、認証情報の設定を行います。下記あたりを参考にすると良いかと思います。リフレッシュトークンの取得はアプリ連携が終わった後の方がいいかも!?しれないです。アプリとの連携前にリフレッシュトークンは取得済みだったのですが、それを使ってもなかなか有効期限が取得できず、再取得したらできるようになりました。(この辺りの手順どこにあったか、見逃したかもしれないです。)
- Getting Started | Google Play Developer API | Google Developers
- Authorization | Google Play Developer API | Google Developers
- Google API OAuth2.0のアクセストークン&リフレッシュトークン取得手順メモ - Qiita
APIにアクセスして、有効期限を取得します。APIのアクセス数には制限がありますので、注意して実装します。例えば、1度購入情報の検証が全体通して問題がなかった場合、サーバーからアプリに有効期限情報を送信し、有効期限内は問い合わせを行わないなどの考慮をすると良いかと思います。毎回アプリの起動(や課金コンテンツを開く)タイミングで問い合わせてたらあっさり超えてしまいそうなので、工夫が必要です。実際の実装の際にもエラー処理を考慮した上で、サーバーへの問い合わせをどの状態の時に行うかを考え、実装するのが非常に苦労しました。APIからの情報取得は下記を参考にすることができます。
- Purchases.subscriptions: get | Google Play Developer API | Google Developers
- Purchases.subscriptions | Google Play Developer API | Google Developers
最後に課金情報をDBに保存したり、有効期限をアプリに送信して、アプリ側で購入完了処理をしたりすることで購入が完了となります。
まとめ
参考
- アプリ内課金 | Android Developers
- Selling In-app Products | Android Developers
- Getting Started | Google Play Developer API | Google Developers
- Authorization | Google Play Developer API | Google Developers
- Purchases.subscriptions: get | Google Play Developer API | Google Developers
- Purchases.subscriptions | Google Play Developer API | Google Developers
- Google API OAuth2.0のアクセストークン&リフレッシュトークン取得手順メモ - Qiita
- 自動購読課金について【Android編】|サイバーエージェント 公式エンジニアブログ
- Androidアプリ内課金の不正購入チェックをPHPで - WonderPlanet DEVELOPER BLOG