share facebook facebook facebook twitter twitter menu hatena pocket slack

2021.04.21 WED

Cordova(Monaca)でアプリ内課金を実装する

西田 駿史

WRITTEN BY 西田 駿史

とりあえずCordovaの消耗(消費)型課金の処理を実装、検証してみたいってときの手順です。

アプリの準備

まずは課金処理を実装したアプリを準備します。
App Store ConnectGoogle Play Consoleでとりあえずアプリの枠は作ってあるものとします。

Cordovaプラグインの追加

プロジェクトにcordova-plugin-purchaseプラグインを追加します。

BILLING_KEYの取得・設定

Google Play Consoleの「収益化のセットアップ」からキーを取得します。
MIIBで始まる文字列です。

取得した文字列をBILLING_KEYとしてcordova-plugin-purchaseのインストールパラメータに設定します。

cordova-plugin-purchaseプラグイン

GitHubにプラグインの解説リンクが載ってます。

To ease the beggining of your journey into the intimidating world of In-App Purchase with >Cordova, we wrote a guide which hopefully will help you get things done: https://purchase.cordova.fovea.cc/

「Cordovaでのアプリ内課金の恐ろしい世界への旅の始まりを容易にするために、私たちはあなたが物事を成し遂げるのに役立つことを願ってガイドを書きました:https://purchase.cordova.fovea.cc/

ずいぶん剣呑な感じですが…。
基本はこれとAPIドキュメントとにらめっこしながら実装していく感じになります。

プラグインの解説

ドキュメントの特に重要そうな部分を和訳しました。
https://purchase.cordova.fovea.cc/discover/about-the-plugin
https://github.com/j3k0/cordova-plugin-purchase/blob/master/doc/api.md#life-cycle

課金アイテムの状態
  • registered:登録済み(製品はプラグインに認識されています)
  • valid:有効(製品の詳細はストアから読み込まれ、製品は購入できます))
    • invalid:無効(製品の詳細を読み込めません)
  • requested:リクエスト済み(注文済み)
  • approved:承認済み(注文は承認済み)
  • finished:終了(アプリは注文を配信しました)
  • owned:所有(製品を所有済)
課金アイテムの購入プロセス
  1. Requesting a purchase.(購入をリクエスト)
  2. Getting approval from the bank(口座情報の確認)
  3. elivery by the application(アプリによる配信)
  4. Finalization(終了処理)

注文が行われた後、ストアプラットフォームは顧客の口座から金額を引き落とすための処理を行います。
引き落とし処理が承認されると、商品は承認された状態(approved)になります。
アプリは承認通知を受け取り、次のことを行う必要があります。

  1. レシートの検証
  2. 商品の提供

アプリを商品をユーザーに納品すると、注文が確定します(finished)。その時、お金はあなたに送金されます。
このようにして、納品できなかった商品に対して顧客に請求されないようにします。

終了処理後、商品は所有(owned)されます。消耗品は再度購入できます。

課金アイテムのライフサイクル

プラグインの実装

Micro Exampleを参考に実装したのがこちら。

index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="viewport-fit=cover, width=device-width, height=device-height, initial-scale=1, maximum-scale=1, user-scalable=no">
        <meta http-equiv="Content-Security-Policy" content="default-src * data: gap: content: https://ssl.gstatic.com; style-src * 'unsafe-inline'; script-src * 'unsafe-inline' 'unsafe-eval'">

        <script src="components/loader.js"></script>
        <link rel="stylesheet" href="components/loader.css">
        <link rel="stylesheet" href="css/style.css">
    </head>

    <body>
        <main class="container"></main>
        <div class="row"></div>
       <textarea id="log" class="container" rows="20" cols="40" disabled></textarea>
       <script>
            console._log = console.log;
            console.log = function(msg) {
                document.getElementById('log').innerHTML += msg + '\n';
                console._log(msg);
            };
            console.log('init');
        </script>
        <script src="js/app.js"></script>
    </body>
</html>

style.css

.container {
    display: flex;
    justify-content: center;
}
.row{
    padding: 10px;
}

app.js

document.addEventListener('deviceready', onDeviceReady);

// デバイスの準備が完了
function onDeviceReady() {
    // プラグインが有効か確認
    if (!window.store) {
        console.log('Store not available');
        return;
    }

    // UIのリフレッシュ
    refreshUI();

    // プラグインに商品を認識させる
    // コードで商品を使用する前に、商品のタイプと識別子を知っている必要がある
    store.register({
        type: store.CONSUMABLE,
        id: 'coins100',
        alias: '100コイン'
    });    // 消耗型商品を登録

    // エラーハンドラを登録
    store.error(function(error) {
        console.info('STORE ERROR ' + error.code + ': ' + error.message);
    });

    // 商品にコールバックを設定
    store.when('coins100')
        .updated(refreshUI) // 商品に変更があった
        .cancelled(cancelled) // キャンセルされた
        // ↓レシート検証する
        .approved(verifyPurchase) // 商品が承認済みになった
        .verified(finishPurchase) // 商品レシートを検証済み
        .unverified(unverifyPurchase) // 商品レシート検証失敗
        // ↓レシート検証しない
        // .approved(finishPurchase) // 商品が承認済みになった
        .error(function(error) {
            console.info('PRODUCT ERROR ' + error.code + ': ' + error.message);
        });

    // ストアに接続して商品の詳細情報を取得(商品情報のハードコーディングはNG)
    store.refresh();
}

// 購入
function purchase() {
    console.log('start purchase');
    store.order('coins100');
    console.log('end purchase');
}

// キャンセルされた
function cancelled(product) {
    console.log('cancelled');
}

// サーバーサイドでレシート検証
function verifyPurchase(product) {
    console.log('start verifyPurchase');
    store.validator = "https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/api/check-purchase";
    product.verify();
    console.log('end verifyPurchase');
}

function unverifyPurchase(product) {
    console.log('unverifyPurchase');
}

// 購入完了処理
function finishPurchase(product){
    console.log('finishPurchase');
    // ゲームコインを追加
    localStorage.goldCoins = (localStorage.goldCoins | 0) + 100;

    // アプリがコンテンツを配信できなかった場合、product.finish()は呼び出されない
    // その場合は、store.refresh()で再度approvedがトリガーされる
    product.finish();
    refreshUI();
}

// UIのリフレッシュ
function refreshUI() {
    console.log('refreshUI');

    // 商品情報を参照
    const product = store.get('coins100');

    if (!product) {
        // 商品情報が取得できない
        return;
    }else if (product.state === store.INVALID) {
        // 商品が無効
        console.log('product invalid');
        return;
    }

    // 商品情報をUIにセット
    const button = `<button onclick="purchase()">Purchase</button>`;

    document.getElementsByTagName('main')[0].innerHTML = `
    <div>
        <pre>
        Gold: ${localStorage.goldCoins | 0}

        Product.state: ${product.state}
                .title: ${product.title}
                .descr: ${product.description}
                .price: ${product.price}

        </pre>
        ${product.canPurchase ? button : ''}
    </div>`;
}

レシート検証API

https://github.com/j3k0/cordova-plugin-purchase/blob/master/doc/api.md#receipt-validation
とりあえず呼び出せてレシートの内容をログに出せればよかったので、簡易的にAWS Chaliceで実装しました。

app.py

from chalice import Chalice
import json
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

app = Chalice(app_name='app-purchase-test-api')

@app.route('/check-purchase', methods = ['POST'], content_types=['application/json'], cors=True)
def index():
    request = app.current_request

    logger.info(json.dumps(request.json_body))

    error_response = {
        'ok': False,
        'data': {
            'code': 6778003,
        },
        'error': {
            'message' : "invalid receipt"
        },
    }

    if 'transaction' in request.json_body:
        logger.info(request.json_body['transaction'])
        transaction = request.json_body['transaction']
    else:
        logger.error('transaction not in body')
        return error_response

    if transaction['type'] == 'ios-appstore':
        # iOSレシート検証処理を書く
        pass

    elif transaction['type'] == 'android-playstore':
        # Androidレシート検証処理を書く
        pass

    else:
        logger.error('type not in transaction')
        return error_response

    return {
        'ok': True,
        'data': {
            'transaction': request.json_body['transaction'],
        },
    }

アプリのビルド

iOSはデバッグビルド、Androidはリリースビルドまで済ませる必要があります。
MonacaでiOSデバッグビルドを実行するまで

但し端末にインストールし実行してもまだ、ストアに商品が登録されていないので何も表示されません。
各プラットフォームに設定していく必要があります。

App Store Connect設定・iOSアプリの実行

App Store Connectを開き課金に必要な設定をやっていきます。
こちらの記事を参考にしました。
App内課金をSANDBOXユーザーでテストする – AppStoreConnect編(2019年版)

契約/税金/口座情報の設定

初めて課金アプリを作る場合は、口座等の設定が必要になります。

課金アイテムの登録

上記が完了していれば課金アイテムがアプリに登録できます。

製品IDはアプリで指定しているものを設定しましょう。

Sandboxアカウント追加

iOSアプリの購入テストでSandboxアカウントを作って使うマニュアル

iOSアプリの実行

ここまでできればiOSで課金の一連の流れが試せます。
デバッグビルドのアプリをインストールしてSandboxアカウントを設定した端末で実行します。

TestFlightで実行する

リリースビルドを作成して、App Store Connectにビルドをアップロード。
TestFlightにビルドを登録して、テスターを登録すればTestFlightアプリで配布ができます。

Google Play Console設定・Androidアプリの実行

Google Play Consoleを開き課金に必要な設定をやっていきます。

販売アカウントの設定

Android有料課金アプリ販売登録方法 – 初心者向け

アルファ版のリリース

iOSと違いアルファ版を一度リリースしなければいけません。
こちらの記事を参考に
Androidアプリの内部テストを実施する

まずはダッシュボードの初期設定を適当でもいいのですべて消化します。

埋め終わったら「クローズドテスト」から「トラックを管理」へ。

新しいリリースを作成します。国とテスターも設定しておきます。

リリースビルドのAPKをアップロード、説明はこんな感じでもいいので審査に回します。

審査が完了してリリース済みになるまで気長に待ちます。

課金アイテムの登録

リリース済みになれば課金アイテムが登録できます。

登録した後に有効化するのを忘れないようにしましょう。

ライセンステスターの登録

購入テストをするために登録が必要です。

Androidアプリの実行

ここまでできればAndroidで課金の一連の流れが試せます。
テスターに登録したアカウントでGoogle Playにログインした端末で試しましょう。
アプリ自体はGoogle Playからインストールしなくても、デバッグビルドでも試せます。

cordova-plugin-purchaseと各アプリプラットフォームの課金のエコシステムを理解するのが非常に大変でした。
端折ったレシート検証についても後日詳しく書こうと思います。

書きました。
cordova-plugin-purchase用の消耗型課金レシート検証APIをLaravelで実装する

元記事はこちら

Cordova(Monaca)でアプリ内課金を実装する

西田 駿史

西田 駿史

2019年4月入社。第四開発事業部グループリーダー。海岸オフィス所属

cloudpack

cloudpackは、Amazon EC2やAmazon S3をはじめとするAWSの各種プロダクトを利用する際の、導入・設計から運用保守を含んだフルマネージドのサービスを提供し、バックアップや24時間365日の監視/障害対応、技術的な問い合わせに対するサポートなどを行っております。
AWS上のインフラ構築およびAWSを活用したシステム開発など、案件のご相談はcloudpack.jpよりご連絡ください。