サブスクリプションを載せた ahha の開発中に、こういう症状に出会いました。
Google Play で購読を解約した。ところがアプリは相変わらず「プレミアム」と表示しています。
- アプリ内の「状態を更新」ボタンを押しても変わらない
- アプリを完全終了して再起動しても変わらない
- 一日置いても変わらない
二度の空振り
私はこれをアプリのバグだと考え、二度にわたって直しました。
一度目 — Entitlement ID の確認
RevenueCat の Entitlement 識別子が、ダッシュボードの設定とコードで一致しているかを確認しました。一致していました。症状そのまま。
二度目 — SDK キャッシュの回避
キャッシュが古いのかと思い invalidateCustomerInfoCache() を入れて強制照会に変えました。三層の安全網(起動時照会・アプリ復帰時の再照会・キャッシュ期限の検証)まで敷きました。症状はそのままでした。
ここで気づくべきでした。キャッシュを無効化してサーバーに直接聞いているのに答えが変わらないなら、サーバーが持っている値そのものが間違っているということです。
原因 — 通知経路が最初から繋がっていなかった
RevenueCat ダッシュボードの Play Store アプリ設定画面を開くと、こんなエラーが出ていました。
Google Cloud Pub/Sub API must first be enabled
その下の「Google developer notifications」欄は Topic ID が空で、「Connect to Google」ボタンは押せない状態でした。
[私が思っていた構造]
Google Play ──通知──> RevenueCat ──照会──> アプリ
[実際の構造]
Google Play ✕ RevenueCat ──照会──> アプリ
(経路なし)
Google が解約の事実を RevenueCat に伝える通路が、最初から存在していませんでした。
アプリがどれだけキャッシュを無効化して強制照会をしても、問い合わせ先である RevenueCat のサーバー自体が古い状態を持っています。アプリのコードでは絶対に解決できない問題でした。
Google 側の設定 — 罠がひとつある
1. GCP プロジェクトで Cloud Pub/Sub API を有効化
Google Play Console と紐づいている GCP プロジェクトで Pub/Sub API を有効にします。ここが無効だと以降の手順が全部塞がれます。
2. RevenueCat のサービスアカウントに権限を付与
RevenueCat が発行したサービスアカウントにPub/Sub 管理者のロールを与えます。
ここで罠にはまりました。ロール一覧を検索すると、こういうものが並びます。
Pub/Sub 管理者 Pub/Sub Lite 管理者 ← これを選んでしまった Pub/Sub 編集者 Pub/Sub 購読者
Pub/Sub Lite はまったく別のサービスです。名前が似ているので選んでしまい、権限を与えたのに接続できずしばらく迷いました。
3. RevenueCat で Topic を接続
RevenueCat ダッシュボード → 該当アプリ → Google developer notifications で Topic を選び、「Connect to Google」を押します。
4. 実際に受信しているかを確認する
ここが肝心です。接続ボタンを押したら終わりではありません。「Last received」に実際の受信時刻が入るかを見ます。
入らなければどこかで切れています。テスト購読をひとつ作って状態を変えてみると、通知が来るかどうかが分かります。
5. Play Console にトピック名を登録
Google Play Console → 収益化の設定 で Pub/Sub トピック名を入力して保存します。
スポンサーリンク
スポンサーリンク
Apple 側は簡単だった
RevenueCat ダッシュボードの App Store アプリ設定に「Apply in App Store Connect」ボタンがあります。これを押すと RevenueCat が App Store Connect に Production・Sandbox 両方の通知 URL を自動登録します。
登録後に状態表示が configured correctly になるかを確認すれば完了です。
エラーバナーはすぐ消える
デバッグ中に踏んだ小さな罠をひとつ。
RevenueCat ダッシュボードのエラーバナーは、画面に入った直後だけ表示されて消えることがあります。他を確認してから戻ると、もうありません。
エラーバナーを見たらその場でスクリーンショットを撮っておくのが安全です。私はこれを逃して「確かに赤いものがあったはずだ」と画面を何度も再読み込みしました。
アプリ側にも問題があった — boolean では表せない
通知経路を直すと、二つ目の問題が見えてきました。
解約は即終了ではありません。Apple も Google も、解約しても支払い済みの期間中は購読が有効です。entitlements.active にそのまま残ります。
ところが私のアプリの状態モデルはこうでした。
const isPremiumVar = makeVar<boolean>(false);
boolean ひとつでは「解約したがまだ利用できる」を表す方法がありません。そのため解約直後にアプリを見ても「プレミアム」としか出ず、ユーザーは「解約できていない」と誤解します。
状態をこう分けました。
export type PlanStatus =
| { kind: "unknown" } // サーバー確認前
| { kind: "free" } // 無料
| { kind: "active"; expiresAt: number | null } // 有料・更新予定
| { kind: "cancelled"; expiresAt: number | null } // 解約済み・期間残あり
| { kind: "billingIssue"; expiresAt: number | null }; // 支払い問題が発生
RevenueCat の CustomerInfo から、こう判定します。
/**
* CustomerInfo から PlanStatus を導出する。
*
* billingIssue を先に判定するのは、支払い問題の復旧導線が最優先だからである。
* 解約判定に unsubscribeDetectedAt を先に置く理由は本文を参照。
*/
function toPlanStatus(customerInfo: CustomerInfo): PlanStatus {
const entitlement = customerInfo.entitlements.active[PREMIUM_ENTITLEMENT_ID];
if (!entitlement) return { kind: "free" };
const parsed = entitlement.expirationDate
? Date.parse(entitlement.expirationDate)
: null;
const expiresAt = parsed !== null && Number.isFinite(parsed) ? parsed : null;
if (entitlement.billingIssueDetectedAt) {
return { kind: "billingIssue", expiresAt };
}
if (entitlement.unsubscribeDetectedAt || !entitlement.willRenew) {
return { kind: "cancelled", expiresAt };
}
return { kind: "active", expiresAt };
}
機能へのアクセス権は cancelled・billingIssue でも有効です。表示文言だけを分岐させます。
/**
* プラン状態が「Premium 系」かどうかを判定する。
*
* cancelled・billingIssue も entitlement は有効なので機能アクセスは許可する。
* 表示文言だけを状態ごとに分岐させる。
*/
export function isPremiumPlan(status: PlanStatus): boolean {
return (
status.kind === "active" ||
status.kind === "cancelled" ||
status.kind === "billingIssue"
);
}
unsubscribeDetectedAt を先に見る理由
解約判定に willRenew だけを使わなかったのには理由があります。
willRenew はサーバー通知を受けて更新される値です。通知が遅れると、解約したのに true のまま残ります。unsubscribeDetectedAt は「解約されたがまだ有効な購読」を検出するために RevenueCat が推奨しているフィールドです。
両方を見るのが安全です。
まとめ
決済・購読の不具合は、アプリのコードを漁る前に通知経路を確認します。RTDN(Google)・ASSN(Apple)が実際に接続され、実際に受信しているかどうか。これを先に見ていれば、二つのセッションを無駄にせずに済みました。
「アプリを直したのに直らない」が二度続いたら、原因がアプリの外にある可能性を先に検討します。同じ階層で三つ目の処方を試す前に、一階層外を疑うほうが速い。
解約は即終了ではありません。状態モデルを boolean にすると、この区間を表現できず、ユーザーは解約が失敗したと誤解します。
この記事について
自社アプリ ahha(React Native + Expo Bare Workflow・RevenueCat サブスク・AdMob)の開発中に踏んだ内容です。画面の経路は 2026 年 8 月時点のもので、RevenueCat のダッシュボード UI は変更が頻繁です。
Pub/Sub 管理者ロールが最小権限かどうかは検証していません。より狭いロールでも動く可能性があります。また App Store 側(ASSN)は RevenueCat の自動登録ボタンで処理したため、手動設定の場合に必要な手順は確認できていません。
スポンサーリンク
スポンサーリンク



コメント