◉RORK LABEN
●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SONNET 4.5 — Claude API での提供終了は 11/30、残り27日。API 連携を足したアプリは移行先の確認を●SDK 58 — React Native 0.88 に更新されました。Android のリリース版で R8 が既定オンになり、iOS は SceneDelegate を生成します●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SONNET 4.5 — Claude API での提供終了は 11/30、残り27日。API 連携を足したアプリは移行先の確認を●SDK 58 — React Native 0.88 に更新されました。Android のリリース版で R8 が既定オンになり、iOS は SceneDelegate を生成します●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…
記事一覧/開発ツール
⬡ 開発ツール/2026-06-15上級

解約だと思って権限を切ったユーザーは、まだ払う気だった — 支払い猶予期間と権限維持の実装

購読の支払いが失敗しても、それは解約ではありません。猶予期間中に権限を切ると復帰可能な売上を自分で捨てます。Rork(Expo)アプリで猶予・課金リトライ・アカウントホールドを取り違えず、権限維持と復帰導線を出す実装をまとめます。

サブスクリプション66RevenueCat30課金9Expo234収益化70

✦ プレミアム記事

購読アプリを運用していて、ある月の解約数が妙に多い、と感じたことがあります。調べてみると、その「解約」の半分以上は本人が解約ボタンを押したわけではなく、クレジットカードの期限切れや限度額超過で自動更新の決済が失敗していただけでした。問題は、私のアプリがその状態を「解約」と同じ扱いにして、即座に有料機能を閉じていたことです。

支払いが一度失敗しただけのユーザーは、まだ払う気があります。Apple も Google も、決済が失敗した購読をすぐには切らず、数日から数週間かけて自動でリトライする仕組みを用意しています。この期間に権限を維持して「カードを更新してください」と促せるかどうかで、回収できる売上が変わります。私の場合、ここを直しただけで失敗課金からの復帰がはっきりと増えました。

ここからは Rork で生成した Expo アプリを前提に、支払い失敗の4つの状態を取り違えずに扱い、猶予期間は権限を維持し、本当に回収不能になったタイミングで初めて権限を切る実装をまとめます。決済の状態管理そのものに関心がある方は、先にサブスク権限のエンタイトルメント状態機械の設計を読んでおくと、本記事の state machine が頭に入りやすいはずです。

「支払い失敗」は1つの状態ではない

失敗から失効までに挟まる4つの中間状態

最初の落とし穴は、isActive === false を見て「解約」と判断してしまうことです。実際には、自動更新の決済が失敗してから購読が完全に消えるまでに、いくつもの中間状態があります。

  • 課金リトライ(Billing Retry): 決済が失敗し、ストアが自動でリトライしている期間。この間も多くの場合は権限を維持すべきです。
  • 支払い猶予期間(Billing Grace Period): 開発者が明示的に有効化する猶予。期間中はユーザーが有料機能を使い続けられます。Apple は購読周期に応じて最大16日、Google は週次で最大7日・月次以上で最大30日が目安です。
  • アカウントホールド(Account Hold): 猶予もリトライも尽きた状態。ここで初めて権限を停止します。ただしユーザーがカードを直せば購読は復活します。
  • 解約・失効(Cancelled / Expired): 本人が解約した、またはリトライ期間も終わって購読が完全に終了した状態。

重要なのは、リトライと猶予の段階では「まだ顧客である」と見なすことです。ここで権限を切ると、ユーザーは「金は払ったのに使えなくなった」と感じ、復帰どころか低評価レビューに直行します。

ストア固有の状態を1つの列挙型に正規化する

私はこの4状態を、アプリ全体で1つの列挙型に正規化してから扱うようにしています。ストアごとの細かい通知タイプを画面側まで持ち込まないことが、バグを減らす最大のコツです。

// subscription/types.ts
// ストア固有の状態を、アプリ内では必ずこの5値に正規化する
export type EntitlementState =
  | 'active'        // 正常に課金中
  | 'grace'         // 決済失敗だが猶予/リトライ中(=権限は維持する)
  | 'hold'          // アカウントホールド(=権限は停止するが復帰可能)
  | 'expired'       // 完全に失効(解約・期限切れ)
  | 'never';        // 一度も購読していない
 
export interface EntitlementSnapshot {
  state: EntitlementState;
  // grace のときに「いつまで使えるか」を画面に出すために保持する
  graceUntil: string | null;   // ISO8601、なければ null
  willRenew: boolean;          // 次回自動更新が予定されているか
  productId: string | null;
}

RevenueCat の customerInfo から状態を読む

RevenueCat を使っている場合、customerInfo に支払い失敗の手がかりが入っています。多くの実装が entitlements.active の有無だけを見ていますが、それだと猶予期間を取りこぼします。猶予中は isActive が true のまま willRenew が false になり、billingIssueDetectedAt に失敗検知時刻が入る、という組み合わせを読む必要があります。

// subscription/fromRevenueCat.ts
import Purchases, { CustomerInfo } from 'react-native-purchases';
import { EntitlementSnapshot } from './types';
 
const ENTITLEMENT_ID = 'premium'; // RevenueCat で設定した Entitlement 識別子
 
export function toSnapshot(info: CustomerInfo): EntitlementSnapshot {
  const ent = info.entitlements.active[ENTITLEMENT_ID]
    ?? info.entitlements.all[ENTITLEMENT_ID];
 
  if (!ent) {
    return { state: 'never', graceUntil: null, willRenew: false, productId: null };
  }
 
  const hasBillingIssue = ent.billingIssueDetectedAt != null;
 
  // isActive が true なら、まだ使わせてよい期間(正常 or 猶予)
  if (ent.isActive) {
    if (hasBillingIssue && !ent.willRenew) {
      // 決済は失敗したが、有効期限までは猶予として使える
      return {
        state: 'grace',
        graceUntil: ent.expirationDate ?? null,
        willRenew: false,
        productId: ent.productIdentifier,
      };
    }
    return {
      state: 'active',
      graceUntil: null,
      willRenew: ent.willRenew,
      productId: ent.productIdentifier,
    };
  }
 
  // isActive が false。ここで hold と expired を分けるのが肝心
  // RevenueCat 単体では両者の区別が曖昧なため、サーバー側の通知で補強する(後述)
  return {
    state: hasBillingIssue ? 'hold' : 'expired',
    graceUntil: null,
    willRenew: false,
    productId: ent.productIdentifier,
  };
}

ここで billingIssueDetectedAt が null でないかどうかが、解約とホールドを分ける最初の判断材料になります。本人が解約した場合は課金問題が立っていないため expired、決済失敗起因で失効した場合は課金問題が立っているため hold に倒せます。

ただしクライアントの customerInfo だけでは取りこぼしが出ます。アプリが起動していない間に状態が変わるからです。そこでサーバー側の通知を一次情報として併用します。

✦

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
✦支払い失敗ユーザーを『解約済み』として権限を切り、復帰できたはずの売上を取りこぼしていた人が、猶予期間を維持する実装を今日手に入れられる
✦RevenueCat の customerInfo と App Store Server Notifications V2 / Google RTDN を突き合わせ、猶予・リトライ・ホールド・解約を区別する動くコードを習得できる
✦猶予中はソフトに、ホールド突入で明確に、という二段階の復帰導線を実装し、失敗課金からの回収率を計測できる状態になれる
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

⟐ ビジネス2026-05-05
Rork Max × RevenueCat Paywalls SDK で変わるペイウォール開発 — A/B テスト・リモート更新・コンバージョン改善の実装ガイド
RevenueCat Paywalls SDK を Rork Max アプリに統合し、審査なしでペイウォールをリモート更新・A/B テストする実装ガイド。コンバージョン率を改善するデザイン原則と実践コード、必要なサンプル数の計算までをまとめます。
⟐ ビジネス2026-04-14
Rork Maxで個人開発アプリ月収100万円を達成する:収益設計・実装・データ分析の設計書
Rork Maxで月収100万円を目指す収益設計をまとめます。勝てるジャンルを選ぶ5つの判断基準、3層構造の収益モデル、RevenueCatで実装する課金フロー、広告費ゼロからのユーザー獲得、KPIの読み方と改善サイクル、よくある失敗の回避策までを体系的に扱います。
⟐ ビジネス2026-04-04
Rork アプリの収益設計 — 広告と課金が食い合う地点をどう避けるか
Rork で作ったアプリに AdMob・サブスクリプション・単発課金をどう重ねるか。広告と課金のカニバリゼーションを設計で避ける方法、RevenueCat の Entitlement 中心設計、ペイウォールを出すタイミングの決め方を実装コードとともにまとめました。