Cordovaで実装するアプリ内課金(IAP):cordova-plugin-purchase徹底ガイド

Cordovaで実装するアプリ内課金(IAP):cordova-plugin-purchase徹底ガイド

はじめに (対象読者・この記事でわかること)

本記事は、Cordova/ハイブリッドアプリでアプリ内課金(In‑App Purchase, IAP)を実装したいフロントエンド・モバイル開発者を対象としています。JavaScriptから決済フローを安全に扱うための設計指針、iOS/Androidの両ストアに対応するベストプラクティス、審査で落ちないための注意点を、実コードとともに解説します。

読み終える頃には、cordova-plugin-purchase(旧cc.fovea.cordova.purchase)を使った消費型・非消費型・サブスクリプションの基本実装、レシート検証方針、テスト・デバッグ方法まで一通り理解し、実運用へ移行できる状態になります。


開発環境・前提知識

この記事で紹介する内容は、以下の環境で動作確認をしています。

カテゴリ バージョン/情報
OS macOS Sonoma 14.6 / Windows 11
言語/FW Cordova 12.x / Node.js 20.x
ライブラリ cordova-plugin-purchase 13.x, cordova-ios 7.x, cordova-android 13.x

また、この記事を読み進める上で、以下の知識があるとスムーズです。 * JavaScript/TypeScriptの基礎(Promise/イベント駆動) * iOS/Androidのビルド環境(Xcode/Android Studio)と各ストアの基本操作(App Store Connect / Google Play Console)


なぜCordovaでアプリ内課金なのか:概要・背景

ネイティブ開発に比べ、CordovaはWeb技術でモバイルアプリを横断的に開発できる反面、アプリ内課金のようなOS固有機能はプラグイン頼みになります。cordova-plugin-purchaseは、Apple App Store(StoreKit)とGoogle Play Billingを抽象化し、単一のJavaScript APIで課金フロー(商品情報取得、購入、復元、所有状況の同期)を扱える成熟度の高いプラグインです。

IAPの実装で重要なのは「正しい商品設計(消費型/非消費型/サブスク)」「同期と復元の正確性」「レシート検証」「エラーハンドリング・リトライ」「テストモード・審査要件対応」の5点です。これらを怠ると、二重課金や未付与、審査リジェクト、返金トラブルにつながります。本記事では、これらの課題をCordovaの文脈で解きほぐし、短期間で堅牢なIAP基盤を築くための実践ガイドを提供します。


実装手順とベストプラクティス:cordova-plugin-purchaseで作るIAP

ここが記事のメインパートです。プロジェクト準備から商品定義、購入フロー、復元、レシート検証、サブスク、テスト・審査対応まで段階的に解説します。

ステップ1:環境準備とプラグイン導入

1) Cordovaプロジェクト作成とプラットフォーム追加

npm i -g cordova
cordova create com.example.iap IapApp IapApp
cd IapApp
cordova platform add ios
cordova platform add android

2) プラグインのインストール(AndroidのBilling権限同期を含む)

cordova plugin add cordova-plugin-purchase
# 必要に応じてTypeScript型
npm i -D @types/cordova

3) ストア設定 - App Store Connectでアプリ登録、IAPを有効化し、製品ID(例: com.example.iap.coin100)を作成。契約/銀行/税務を完了。 - Google Play Consoleでアプリ登録、アプリ内アイテム(管理対象アイテム/定期購入)を作成。ライセンステストアカウントを設定。

4) iOSの機能設定 - XcodeのSigning & Capabilitiesで「In‑App Purchase」を有効化。 - サンドボックステスターをApple IDアカウントで作成。

5) Androidの注意点 - Billing Library v5+が必要な場合、プラグインの互換バージョンを使用(cordova-android 12+推奨)。 - ライセンステストにGoogleアカウントを登録。

初回はストア側の商品が「承認待ち/アクティブ」になるまで反映に時間がかかる点に注意。

ステップ2:商品定義と初期化

製品IDはストア側とアプリ側で完全一致させます。消費型(Consumable)、非消費型(Entitlement/Non‑Consumable)、サブスク(Subscription)を用途に応じて使い分けます。

// www/js/iap.js
document.addEventListener('deviceready', () => {
  const store = window.store; // cordova-plugin-purchase のグローバル

  // ログを有効化(デバッグ時)
  store.verbosity = store.DEBUG;

  // 製品の登録(ストアに存在するIDと一致させる)
  store.register([
    { id: 'com.example.iap.coin100', type: store.CONSUMABLE },
    { id: 'com.example.iap.premium', type: store.NON_CONSUMABLE },
    { id: 'com.example.iap.pro.monthly', type: store.SUBSCRIPTION }
  ]);

  // 製品情報が取得されたらUIを更新
  store.when('product').updated((p) => {
    // p.title, p.price, p.description などで表示
    console.log('Product updated:', p.id, p.title, p.price);
  });

  // 承認フロー:購入後、権利付与してfinish
  store.when('com.example.iap.coin100').approved((p) => grantAndFinish(p));
  store.when('com.example.iap.premium').approved((p) => grantAndFinish(p));
  store.when('com.example.iap.pro.monthly').approved((p) => validateAndFinish(p));

  // エラー監視
  store.error((err) => {
    console.error('IAP Error:', err.code, err.message);
    // UI通知、再試行誘導など
  });

  // 初期化
  store.refresh(); // ストアから製品/購入情報を同期
});

function grantAndFinish(p) {
  // クライアント付与例(消費型:コイン付与)
  if (p.id === 'com.example.iap.coin100') {
    addCoins(100);
  }
  // 非消費型:フラグを永続化
  if (p.id === 'com.example.iap.premium') {
    setPremium(true);
  }
  p.finish(); // トランザクションを完了
}

async function validateAndFinish(p) {
  // サブスク/非消費型ではサーバー側でレシート検証するのが安全
  try {
    await postReceiptToBackend(p);
    p.finish();
  } catch (e) {
    console.error('Receipt validation failed', e);
    // finishは保留し、後で再試行(プラグインが再通知)
  }
}

ポイント - store.register後にstore.refreshを呼ぶと、価格やローカライズ情報が入る。 - approvedでの付与は一度だけ。finishを忘れると二重通知や保留が残る。 - サブスクは必ずサーバーでレシート検証して権利付与を行うのが原則。

ステップ3:購入・復元フローのUIとコード

購入

function buy(productId) {
  const store = window.store;
  const p = store.get(productId);
  if (!p || !p.canPurchase) {
    alert('購入できる状態ではありません。後でもう一度お試しください。');
    return;
  }
  p.order(); // 購入開始
}

購入状態の監視

store.when('product').approved(p => console.log('Approved:', p.id));
store.when('product').owned(p => console.log('Owned:', p.id));
store.when('product').cancelled(p => console.log('Cancelled:', p.id));
store.when('product').error(err => console.error('Purchase error:', err));

購入の復元(iOSの必須UI)

function restore() {
  window.store.refresh(); // iOSでは購入履歴を同期
  // 追加でiOSのrestoreCompletedTransactionsに相当する動きが走る
}

所有確認

function isPremium() {
  const p = store.get('com.example.iap.premium');
  return p && p.owned === true;
}

消費型の消費 - cordova-plugin-purchaseはapproved→finishで自動的に消費される(Google Play)。 - 追加で明示的に消費APIは不要だが、finishを確実に呼ぶこと。

ステップ4:サーバーでのレシート検証(推奨)

なぜ必要か - クライアントは改ざん可能。権利付与はサーバー判定が安全。 - サブスクの有効期限、キャンセル、返金反映を正しく扱える。

方針 - クライアント側でp.transactionまたはp.receiptを取得してサーバーへPOST。 - サーバーはApple/Googleの公式APIで検証し、結果をDBに保存。 - アプリ起動時にサーバーへ権利状態を問い合わせ、UIへ反映。

クライアント送信例

async function postReceiptToBackend(p) {
  const body = {
    productId: p.id,
    platform: device.platform === 'iOS' ? 'ios' : 'android',
    transaction: p.transaction, // transactionId, type, appStoreReceiptなど
    receipt: p.receipt // AndroidではpurchaseTokenを含むことがある
  };
  const res = await fetch('https://api.example.com/iap/validate', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
    credentials: 'include'
  });
  if (!res.ok) throw new Error('backend validation failed');
  return res.json();
}

サーバー側の検証(概要) - iOS: App Store Server API(JWTベース)または従来のverifyReceipt(Sandbox/Production切り替え)。 - Android: Google Play Developer API(purchases.products / purchases.subscriptions)でpurchaseToken検証。 - Webhook/RTDN(Apple Server Notifications v2 / Google Real‑time developer notifications)を受け、権利変動を非同期で反映。

注意 - サンドボックスと本番のエンドポイントを間違えない。 - 時差・有効期限・グレース期間を正しく扱う。 - 冪等性(同一transactionの多重処理防止)を確保。

ステップ5:サブスクリプションの扱い

UIのコツ - 現在のプラン、有効期限、次回請求日、解約導線(ストアの管理画面へのDeep Link)を明示。 - iOS: itms-apps://apps.apple.com/account/subscriptions - Android: https://play.google.com/store/account/subscriptions

ステップ6:テスト・デバッグ

iOS - サンドボックステスターでログイン(設定のApp Storeアカウントをサンドボックスに切替)。 - 価格はテスト用値段、更新は短縮間隔で発生。 - SKError・領収書のSandbox/Production混線に注意。

Android - ライセンステスター(internal testingやclosed testing)で実機テスト。 - 購入は即時、定期は短縮周期。 - Play Console側の反映遅延に注意。古いキャッシュはアプリ再インストールでクリア。

デバッグTips

store.verbosity = store.DEBUG; // 詳細ログ
store.error(e => alert(`IAP Error: ${e.message}`));

ステップ7:審査・公開時の注意

ハマった点やエラー解決

エラー内容:

E/IAB: Billing service unavailable on device.

解決策: - Google Playが無効/未インストール、古いPlayストア。実機で最新化し、テストアカウントでログイン。

エラー内容:

The operation couldn't be completed (SKErrorDomain error 0)

解決策: - iOSサンドボックスが不安定な場合あり。デバイスを再起動、Apple ID再ログイン、後ほど再試行。ネットワーク/VPNを確認。

症状: - store.refresh後も製品がinvalid 対策: - ストア側の製品IDのスペル/ステータスを確認。アプリのバンドルID一致、地域公開、課税/契約完了をチェック。反映待ちの可能性も。

症状: - approvedが来るのに権利付与が重複 対策: - サーバー側でtransactionIdに対する冪等性キーを導入。クライアントも付与処理を再入可能に。


まとめ

本記事では、cordova-plugin-purchaseを用いたCordovaアプリのアプリ内課金実装を、環境構築から商品登録、購入・復元、レシート検証、サブスク運用、テスト・審査対応まで体系的に解説しました。

この記事を通じ、堅牢で審査に通るIAP実装の全体像が掴めたはずです。今後は価格改定、キャンペーン、サーバー通知の自動反映、分析基盤との連携など、運用最適化も検討していきましょう。


参考資料