「購入ボタンを押したのに、プレミアム機能が使えない」——アプリ開発でこの問い合わせほど対応に困るものはありません。ユーザーは確かに課金しており、Stripe や App Store の管理画面にも購入履歴が残っています。なのに、アプリ側ではいつまでも「無料プラン」のままです。
この問題が生じる原因は1つではありません。RevenueCat の非同期処理のタイミング、StoreKit の Sandbox 特有の挙動、Entitlement ID の設定ミスなど、複数の要因が絡み合っています。ここではRork で RevenueCat を使った課金実装後に発生しやすい「ステータス同期問題」を、実際のデバッグ手順とコードを交えて整理します。
なぜ購入後にステータスが即座に反映されないのか
RevenueCat の customerInfo はリアルタイムではなく、キャッシュと非同期取得を組み合わせた仕組みで動作します。購入完了の直後に Purchases.shared.customerInfo() を呼び出しても、古いキャッシュが返ってくることがあります。
特に iOS の StoreKit を使っている場合、購入トランザクションの完了通知は以下の経路をたどります。
- ユーザーが購入ボタンをタップ
- App Store がトランザクションを処理(数秒〜数十秒かかる場合あり)
- RevenueCat の SDK がトランザクションを検知
- RevenueCat のサーバーに通知が送られ、Entitlement が更新される
customerInfoが最新状態を返すようになる
問題の多くは 3〜5 のステップの間に UI 更新を試みることで発生します。
よくある原因 1:purchasePackage の直後に即座にステータス確認している
最も多いパターンです。以下のようなコードを書いていませんか?
// ❌ よくある間違い
const purchase = async () => {
await Purchases.shared.purchasePackage(selectedPackage);
// purchasePackage の完了直後に customerInfo を取得しても
// 古いキャッシュが返ってくることがある
const info = await Purchases.shared.getCustomerInfo();
if (info.entitlements.active["premium"]) {
navigateToPremiumFeature(); // ← 反映されていないため動かない場合がある
}
};正しくは、purchasePackage の戻り値を使います。purchasePackage は成功時に最新の CustomerInfo を含む結果を返すため、改めて getCustomerInfo を呼ぶ必要がありません。
// ✅ 正しい実装
const purchase = async () => {
try {
const { customerInfo } = await Purchases.shared.purchasePackage(selectedPackage);
// purchasePackage の戻り値に最新の customerInfo が含まれている
if (customerInfo.entitlements.active["premium"]) {
navigateToPremiumFeature();
}
} catch (error) {
if (error.code \!== PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR) {
console.error("購入エラー:", error.message);
}
}
};よくある原因 2:Entitlement ID の名前が一致していない
RevenueCat の管理画面で設定した Entitlement ID と、コード内で参照している文字列が一致していない場合です。大文字・小文字、ハイフンとアンダースコアの違いも区別されます。
// ❌ 管理画面で "Premium" と設定しているのに小文字で参照
if (info.entitlements.active["premium"]) { ... }
// ✅ 管理画面の設定と完全一致させる
if (info.entitlements.active["Premium"]) { ... }デバッグ時は console.log(JSON.stringify(info.entitlements.active)) で実際のキーを確認するのが確実です。
よくある原因 3:Sandbox 環境での更新遅延
開発・テスト時に Sandbox アカウントを使って購入している場合、Sandbox 環境は本番よりも処理が遅れることがあります。特に以下の場面で遅延が発生しやすいです。
- Sandbox での定期購読の初回購入後(RevenueCat サーバーへの通知に数秒かかる場合がある)
- Sandbox でのサブスクリプション更新(iOS Sandbox は更新間隔が短縮されているが、通知のタイミングがずれることがある)
テスト中に「動かない」と感じたら、まず数秒待ってから再試行してみてください。本番環境では発生しない場合も多くあります。
また、RevenueCat の Dashboard で Sandbox purchases タブを開き、購入データが届いているかを確認する習慣をつけましょう。購入データ自体が届いていなければ、SDK の設定(特に API キーが Sandbox 用かどうか)を見直す必要があります。
// iOS Sandbox 用の API キーと本番用の API キーを切り替える
const revenueCatApiKey = __DEV__
? "appl_YOUR_SANDBOX_API_KEY" // Sandbox 用
: "appl_YOUR_PRODUCTION_API_KEY"; // 本番用
await Purchases.configure({ apiKey: revenueCatApiKey });よくある原因 4:customerInfo の更新リスナーを設定していない
RevenueCat SDK には、customerInfo が更新されたタイミングで通知を受け取るリスナーが用意されています。このリスナーを設定していないと、バックグラウンドで Entitlement が更新されても UI に反映されません。
// ✅ アプリ起動時にリスナーを設定する
import Purchases, { CustomerInfo } from "react-native-purchases";
// コンポーネントの useEffect などで設定
useEffect(() => {
const listener = Purchases.addCustomerInfoUpdateListener(
(info: CustomerInfo) => {
// customerInfo が更新されるたびに呼ばれる
const isPremium = \!\!info.entitlements.active["Premium"];
setIsPremiumUser(isPremium);
}
);
// クリーンアップ
return () => {
listener.remove();
};
}, []);このリスナーを使うことで、Webhook 経由でサブスクリプションが更新された場合でも自動的に UI が更新されます。
よくある原因 5:アプリの再起動後にサブスクリプション状態がリセットされる
これはローカル状態管理との組み合わせで起きるパターンです。useState だけで課金状態を管理している場合、アプリを再起動するとリセットされます。
アプリ起動時に必ず getCustomerInfo を呼び出して、最新の状態を取得するようにしましょう。
// ✅ アプリ起動時に常に customerInfo を確認する
const checkSubscriptionStatus = async () => {
try {
const info = await Purchases.shared.getCustomerInfo();
const isPremium = \!\!info.entitlements.active["Premium"];
setIsPremiumUser(isPremium);
} catch (error) {
// エラー時はオフラインを想定してキャッシュを使用
console.warn("customerInfo の取得に失敗:", error.message);
}
};
// App.tsx や useEffect の最初に呼び出す
useEffect(() => {
checkSubscriptionStatus();
}, []);デバッグのための簡易チェックリスト
課金後のステータスが反映されない場合、以下の順番で確認してみてください。
Step 1: RevenueCat Dashboard で購入データを確認する 管理画面 → Customers で該当ユーザーを検索し、購入データが届いているかを確認します。データがなければ SDK 設定の問題です。
Step 2: Entitlement ID を確認する
console.log(JSON.stringify(info.entitlements.active)) でアクティブな Entitlement のキーを確認します。
Step 3: purchasePackage の戻り値を使っているか確認する
購入直後は purchasePackage の戻り値から customerInfo を取得します。
Step 4: リスナーが設定されているか確認する
addCustomerInfoUpdateListener でリスナーを登録し、非同期更新を受け取れるようにします。
Step 5: アプリ起動時に getCustomerInfo を呼んでいるか確認する
アプリの初期化処理で常に最新状態を取得します。
最後に
課金実装のバグは、ユーザーが実際に課金した後に発生するため、信頼の損失に直結します。「購入したのに使えない」という体験は、レビューへの悪影響やサポート工数の増加につながります。
RevenueCat の実装では、購入フローだけでなく、アプリ起動時・フォアグラウンド復帰時・Webhook 受信時のステータス確認を組み合わせる点が肝心です。上記のチェックリストで原因を特定できない場合は、RevenueCat の Customer Center ツールや SDK のデバッグログ(Purchases.setLogLevel(LOG_LEVEL.DEBUG))を活用してみてください。
課金実装の前に React Native の非同期処理を整理したい方にも役立ちます。