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:サブスクリプションの扱い
- 初回トライアル、Intro価格、価格改定の承諾など、プラットフォーム差異あり。
- 更新は自動。アプリ側は都度購入しない。所有状態はサーバーの検証結果で判断。
- 期限が切れたらUIを即時反映。バックグラウンド更新通知を活用。
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}`));
- ネットワーク不安定時はリトライを実装。
- approved後にfinishし忘れると詰まる。アプリ再起動時にapprovedハンドラが再度呼ばれるので、冪等的な付与にしておく。
ステップ7:審査・公開時の注意
- Apple審査では「復元」ボタン必須(非消費型/サブスク)。機能説明も必要。
- 課金が必要な機能は、ログイン不要で審査アカウントから確認できるように(デモアカウントを添付)。
- メタデータに課金ポリシー、プライバシーポリシー、利用規約のURLを記載。
- Googleは定期購入の管理・解約方法の明示が求められる。返金ポリシーの透明性を確保。
ハマった点やエラー解決
エラー内容:
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アプリのアプリ内課金実装を、環境構築から商品登録、購入・復元、レシート検証、サブスク運用、テスト・審査対応まで体系的に解説しました。
- 単一APIでiOS/AndroidのIAPを抽象化し、approved→付与→finishの基本動線を確立
- サブスク/非消費型はサーバー検証と通知連携を前提に、権利を厳密管理
- テスト/審査要件(復元ボタン、アカウント提供、ポリシー表記)を早期に整備
この記事を通じ、堅牢で審査に通るIAP実装の全体像が掴めたはずです。今後は価格改定、キャンペーン、サーバー通知の自動反映、分析基盤との連携など、運用最適化も検討していきましょう。
参考資料
- cordova-plugin-purchase 公式ドキュメント: https://github.com/j3k0/cordova-plugin-purchase
- Apple App Store Server API: https://developer.apple.com/documentation/appstoreserverapi
- Google Play Billing / Developer API: https://developer.android.com/google/play/billing
- App Store審査ガイドライン(IAP関連): https://developer.apple.com/app-store/review/guidelines/
- Real-time developer notifications: https://developer.android.com/google/play/billing/rtdn