💸React Native + Expo でアプリ内課金(IAP)を実装した実践まとめ〜 ストア設定・実装・Backend 検証〜

目次
- 📝はじめに
- ✨全体アーキテクチャ
- 採用技術
- 商品設計(コイン型 IAP)
- 📱iOS:App Store Connect での商品登録
- 設定手順
- 📱Android:Google Play Console での商品登録
- 設定手順
- 🚨なぜ Backend 検証が必須なのか
- ❌ やってはいけない構成
- ✅ 正しい構成
- 🌐バックエンド:(コアロジック)
- 処理の流れ
- 実装(抜粋)
- ✅Apple レシート検証(iOS)
- ✅Google レシート検証(Android)
- 🔍️モバイル側:商品ID管理
- 🔀購入フロー(イベントドリブン)
- 購入イベント受信
- 多重処理防止(必須)
- Backend 検証 → finishTransaction
- なぜこの順番か
- Xcodeで確認する場所
- ビルド前(Xcode)
- iOS Sandbox テスト
- セキュリティ設計まとめ
- まとめ
📝はじめに
React Native + Expo でアプリを作っていると、
アプリ内課金(In-App Purchase / IAP) は避けて通れない機能のひとつです。
ただ、実際にやってみると、
- 購入イベントが二重で飛ぶ
- Sandbox と本番のレシートで挙動が違う
finishTransaction()をいつ呼ぶのが正解か分からない- クライアント側だけで処理していいのか不安
など、実装・設計・運用すべてに落とし穴が多いのが正直なところです。
この記事では、
Expo + react-native-iap を使った実アプリの実装をベースに、
- モバイル側の実装(iOS / Android 両対応)
- バックエンドでのレシート検証
- 冪等性・セキュリティ・監査ログ
- 実運用を見据えた設計判断
まで含めて、かなり踏み込んだ内容をまとめます。
✨全体アーキテクチャ
まずは全体像です。

採用技術
- フロントエンド
- Expo SDK 54
- React Native
- react-native-iap v14
- バックエンド
- Node.js + Fastify
- Prisma + PostgreSQL
- クリーンアーキテクチャ + DI
- 課金モデル
- 消耗型(Consumable)コイン商品
商品設計(コイン型 IAP)
今回のアプリでは、
コインを購入し、アプリ内機能で消費する設計にしています。
| コイン数 | iOS Product ID | Android Product ID |
|---|---|---|
| 100 | ios_coin_100 | android_coin_100 |
| 500 | ios_coin_500 | android_coin_500 |
| 1000 | ios_coin_1000 | android_coin_1000 |
| 3000 | ios_coin_3000 | android_coin_3000 |
- すべて 消耗型(Consumable)
- 残高は Backend で管理
- クライアントは「どの商品を買ったか」だけを送信
この商品設計が、そのまま
App Store Connect / Google Play Console の商品登録内容になります。
📱iOS:App Store Connect での商品登録
商品設計が決まったら、まず iOS 側のストア設定を行います。
ここが終わっていないと、 getProducts() や fetchProducts() は何も返しません。
設定手順
- App Store Connect にログイン
- 対象アプリを選択
- 「アプリ内課金」→「管理」
- 「+」から新規商品を作成
- 商品タイプは 消耗型(Consumable)
- Product ID / 価格 / 表示名を設定
- 日本語ローカリゼーションを追加


この時点で
「商品が App Store 側に存在している」ことが目視できれば OKです。
📱Android:Google Play Console での商品登録
次に Android 側の設定です。
設定手順
- Google Play Console にログイン
- 対象アプリを選択
- 「収益化」→「商品」→「アプリ内アイテム」
- 商品を作成
- Product ID / 名前 / 価格を設定
- ステータスを有効化

※ Android は
内部テストトラックにビルドをアップロードしていないと購入できません。
ここはかなりハマりやすいポイントです。
🚨なぜ Backend 検証が必須なのか
IAP 実装で一番重要な考え方です。
❌ やってはいけない構成
購入成功 → クライアントでコイン付与
これだと、
- アプリ改ざん
- 偽レシート送信
- 同一購入の多重付与
が簡単に成立します。
✅ 正しい構成
購入成功
↓
Backend にレシート送信
↓
Apple / Google API で検証
↓
OKなら権利付与
クライアントは検証しない
この原則を守っています。
🌐バックエンド:(コアロジック)
バックエンドが処理の中心になります。
処理の流れ
- レシート / トークンを SHA-256 でハッシュ化
- 既存購入のチェック(冪等性)
- Apple / Google API で検証
- 監査ログを S3 に保存
- DB トランザクションで権利付与
実装(抜粋)
Javascriptconst receiptHash = hashReceipt(receiptOrToken);
const existing = await purchaseRepo.findByReceiptOrToken(receiptHash);
if (existing) {
return {
verified: true,
purchaseId: String(existing.purchaseId),
message: 'Already processed'
};
}- 同じレシート = 同じハッシュ
- 二重処理は Backend 側で完全防止
- クライアントは
Already processedを成功扱いする
✅Apple レシート検証(iOS)
Javascriptconst result = await this.callAppleAPI(this.productionUrl, receiptData);
if (result.status === 21007) {
// Sandbox レシートは Sandbox URL で再検証
result = await this.callAppleAPI(this.sandboxUrl, receiptData);
}
if (result.status !== 0) {
return { isValid: false };
}ポイント:
- 本番 → Sandbox の自動フォールバック
- タイムアウト必須
- shared secret をログに出さない
✅Google レシート検証(Android)
Javascriptif (data.purchaseState !== 0) {
return { isValid: false };
}
return {
isValid: true,
storeProductId,
orderId: data.orderId,
};
ポイント:
- サービスアカウントで API 認証
purchaseState === 0のみ有効- Product ID は必須
🔍️モバイル側:商品ID管理
iOS / Android の違いは 最初に吸収します。
Javascriptexport const PRODUCT_IDS =
Platform.OS === 'ios' ? IOS_PRODUCT_IDS : ANDROID_PRODUCT_IDS;
export const ALL_PRODUCT_IDS = Object.values(PRODUCT_IDS);
- UI / ロジックは
coin_100のようなキーだけを見る - ストア差分は constants に閉じ込める
📌IAP 初期化と商品取得
Javascriptawait RNIap.initConnection();
const products = await RNIap.fetchProducts({
skus: ALL_PRODUCT_IDS,
});
- 起動時に商品を取得して保持
🔀購入フロー(イベントドリブン)
購入リクエスト
Javascriptawait RNIap.requestPurchase({
request: Platform.OS === 'ios'
? { ios: { sku: productId } }
: { android: { skus: [productId] } },
type: 'in-app',
});
購入イベント受信
JavascriptRNIap.purchaseUpdatedListener(async (purchase) => {
await processTransaction(purchase);
});
多重処理防止(必須)
Javascriptif (processedTransactions.has(transactionId)) return;
processedTransactions.add(transactionId);
- 同じ購入イベントが複数回飛ぶことがある
- 対策しないと二重付与される
Backend 検証 → finishTransaction
Javascriptconst result = await iapService.verifyReceipt(purchase);
if (result.verified) {
await RNIap.finishTransaction({
purchase,
isConsumable: true,
});
}
なぜこの順番か
- 検証前に finish → 危険
- 検証失敗時は あえて呼ばない
- トランザクションは OS に残る
- 次回起動時に再処理できる
Xcodeで確認する場所
ビルド前(Xcode)
Xcode > Signing & Capabilities > +Capabilities- In-App Purchaseが追加されているかを確認

iOS Sandbox テスト
- 設定 > App Store > Sandbox アカウント
- 本番 Apple ID は必ずサインアウト
購入時:

セキュリティ設計まとめ
| 項目 | 対策 |
|---|---|
| レシート保存 | SHA-256 ハッシュのみ DB 保存 |
| 冪等性 | ハッシュで重複検知 |
| 検証 | Apple / Google 公式 API |
| 監査 | S3 に暗号化保存(10年) |
| ログ | 秘密情報を出さない |
| 原子性 | DB トランザクション |
| クライアント | Backend 成功後のみ完了 |
まとめ
- IAP は イベントドリブン + 冪等性
- クライアント検証はしない
finishTransaction()は Backend 成功後- 二重処理対策は必須
実装量は多いですが、
この構成にしておくと 安心して運用できます。





![Microsoft Power BI [実践] 入門 ―― BI初心者でもすぐできる! リアルタイム分析・可視化の手引きとリファレンス](/assets/img/banner-power-bi.c9bd875.png)
![Microsoft Power Apps ローコード開発[実践]入門――ノンプログラマーにやさしいアプリ開発の手引きとリファレンス](/assets/img/banner-powerplatform-2.213ebee.png)
![Microsoft PowerPlatformローコード開発[活用]入門 ――現場で使える業務アプリのレシピ集](/assets/img/banner-powerplatform-1.a01c0c2.png)


