この記事は、neccoのアドベントカレンダー2025 3日目の記事です!

2025年7月、App Storeに公開している個人開発のiOSアプリ「Kanau」に、RevenueCatを使って有料サブスクリプション機能を実装しました。 そこで、RevenueCatを使って有料サブスクリプションを追加した際の手順をご紹介します。

アプリの概要

緑からピンクのグラデーションの背景の中央にスマートフォンと文房具が配置され、アプリ名「kanau」が大きく表示されているウェブサイトのスクリーンショット。
iOSアプリ「Kanau

Kanauは、「つくる人のための計算メモ帳」をテーマに、Angularフレームワークで作られた、Ionic/Capacitorアプリです。 Ionic/Capacitorとは、ウェブ技術であるHTMLやCSS、Javascriptを使って、ウェブアプリとネイティブアプリ(iOS/Android)の両方を開発できるフレームワークです。Angular以外にも、ReactやVueなどの様々なフレームワークに対応しています。Capacitorによって、Kanauは、一つのソースコードで、ウェブブラウザとiOSアプリの両方で利用できるようになっています。

今回は、下記の条件を満たすように、RevenueCatを使った有料サブスクリプション機能を実装します。

  • iOSアプリで課金しているユーザーは、ウェブアプリでも有料ユーザーとして扱い、同じ機能を利用できるようにする
  • 課金ユーザーかどうかの判定はFirebaseで一元管理する
  • 開発者アカウントでは、デバッグ用に有料機能のON/OFFを可能にする

なぜRevenueCatを利用するのか

iOSアプリに課金機能をつけるには、最低限、以下の2つの工程が必要です。

  1. App Store Connectで有料機能を商品として追加する
  2. アプリ内で商品の購入や復元処理を実装する

私のアプリは、iOSのほかにウェブブラウザでも利用可能なため、上記に加えて、下記を満たす必要があります。

  • 課金情報をウェブ版とモバイル版で共有できるようにする
  • (将来的に)ウェブ版からもサブスクリプション登録できるようにする

上記をすべて手動でイチから実装すると、かなり作業が多くなりそうだと判断しました。 そこで、RevenueCat@revenuecat/purchases-capacitorプラグインの導入を検討しました。

RevenueCatとは

RevenueCatは、モバイルアプリのサブスクリプション管理を専門とするSaaSです。 あらゆるプラットフォームで課金対応を取り入れるためのプラグインが開発されており、そのうちと一つとして、Capacitorアプリに対応したプラグインがあります。

RevenueCatプラグインを利用すれば、課金の組み込みや、サブスクリプション状況の管理をRevenueCatに任せられます。 ダッシュボードも充実しており、Slack通知などの機能もあります。 iOS/Android/ウェブでの課金状況を一括で管理できるのが大きなメリットです。

個人開発ユーザーはほぼ無料で使える

RevenueCatは、月間トラッキング収益(MTR)が2,500ドルまでは完全無料で利用できます。超過した場合は、超過分に対して1%の手数料が発生します。私のような個人開発ユーザーの場合は、月間収益が2,500ドルを超えることは、まずありません。 手軽に、無料で導入できるとなれば、試さない手はないです。

実装の流れ

今回はすでにストアで公開済の無料アプリへの課金機能追加のため、Apple Developer Programへの加入や、Firebaseを利用した基本的なアプリケーション実装などが終わっている前提で話を進めます。
まずは、RevenueCatでアカウントを作り、その後に具体的な実装を行い、テストを行い、公開します。

  1. RevenueCatアカウント開設
  2. App Store Connectで商品登録
  3. RevenueCat上で商品設定
  4. purchases-capacitorプラグイン導入
  5. サブスクリプション機能実装
  6. サンドボックスアカウントを作成
  7. 実機テスト
  8. 公開申請

実装前の準備

App Store Connectでの準備

まずは、App Store Connectで、商品(サブスクリプション)を登録します。

App Store Connectでアプリを開き、Monetizationタブの「Subscriptions」を開きます。 Subscription Groupsの横の「+」ボタンをクリックし、サブスクリプショングループを作成します。

Apple App Store Connectのサブスクリプション設定画面が表示されている。
※スクリーンショットは、すでにサブスクリプションを作成し終わった状態で撮影しています。

グループの中に、サブスクリプションを作成します。
表示されている登録項目に沿って、サブスクリプションの名称や価格、期間、ローカライゼーション情報、そして審査用の機能のスクリーンショットを追加し、保存します。

アプリを全世界で公開する場合、価格を一つ一つ設定する必要があるのですが、基準とする価格から自動的に他の国の通貨に換算してくれる機能がついており、それほど手間なく設定できました。換算後に、自分で価格を調整することも可能でした。

Apple App Store Connectのサブスクリプションの詳細設定画面のスクリーンショット

RevenueCatのセットアップ

まずはRevenueCatアカウントを開設します。アカウントは無料で開設できます。

RevenueCatのダッシュボードにアクセスし、アプリのプロジェクトを作成します。
Product Catalogに、先ほどApp Store Connectで作成したサブスクリプションを登録します。

サブスクリプション管理プラットフォームRevenueCatのダッシュボードのスクリーンショット。左側のナビゲーションには「Overview」「Charts」「Customers」「Product catalog」「Paywalls」「Targeting」「Experiments」「Web」「Customer Center」などのメニュー項目と、「Apps & providers」「API keys」「Integrations」「Project settings」のセクションがあります。中央には6つの主要なメトリックカードがあり、「Active Trials (0 in total)」「Active Subscriptions (0 in total)」「MRR $0 (Monthly Recurring Revenue)」「Revenue $0 (Last 28 days)」「New Customers 14 (Last 28 days)」「Active Customers 18 (Last 28 days)」が表示されています。「New Customers」と「Active Customers」のカードには折れ線グラフが表示されています。下部には「Recent transactions」のセクションがありますが、「No live transactions to show」と表示されています。

なお、RevenueCatには、Entitlements/Offerings/Packages/Productsといった独自の概念があり、商品の管理にあたってそれらを理解する必要があります。
これらの概念の説明や、具体的な登録手順はRevenueCatを使ってFlutterにアプリ内課金をさくっと導入する(iOS編)がとても参考になりました。 先に進む前に、ぜひご一読をおすすめします。

課金機能を実装する

いよいよ、アプリに課金機能を実装していきます。

アプリの「**設定**」画面のスクリーンショットです。画面はいくつかのセクションに分かれており、「設定」セクションには「アカウント設定」「表示設定」「言語設定」「ログアウト」の項目があります。その下に「**有料機能**」セクションがあり、赤線で囲まれた中に「検索機能サブスクリプション」として「検索機能とプロジェクト公開機能が利用できます」「月額サブスクリプション ¥100」「もっと詳しく」ボタンが含まれています。さらに「購入の復元」項目と「復元する」ボタンが表示されています。続く「サポート」セクションには「使い方ガイド」「サポート」があり、「情報」セクションには「利用規約」「プライバシーポリシー」「ライセンス情報」があります。画面下部のタブバーには「プロジェクト一覧」「プロジェクト」「タグ」「設定」のアイコンとラベルが表示されています。

Xcodeで課金設定を有効化する

XcodeでiOSプロジェクトを開き、「+Capability」をクリックして、「In-App Purchase」を追加します。

macOSのXcodeウィンドウのスクリーンショットで、アプリの署名と機能設定画面が表示され、「Signing & Capabilities」タブと「In-App Purchase」の項目がオレンジ色の枠で囲まれている。

プロジェクトにRevenueCat SDKを導入する

下記のコマンドで、プロジェクトに@revenuecat/purchases-capacitorプラグインを導入します。

npm install @revenuecat/purchases-capacitor
npx cap sync

1. RevenueCat SDKの初期化

アプリ起動時に、RevenueCat SDKを初期化して、Firebase AuthのユーザーIDと紐付けるための記述を追記します。
課金情報をRevenueCatを通じてFirebaseに紐づけることで、iOSでの購入情報を、ウェブアプリからも参照できるようになります。

// app.component.ts
import { Platform } from "@ionic/angular";
import { Purchases, LOG_LEVEL } from "@revenuecat/purchases-capacitor";
import { onAuthStateChanged } from "firebase/auth";

export class AppComponent {
  static purchasesConfigured = false;

  constructor(
    private platform: Platform,
    private authService: AuthService,
  ) {
    this.platform.ready().then(async () => {
      await Purchases.setLogLevel({ level: LOG_LEVEL.DEBUG });

      // RevenueCat Capacitorプラグインはネイティブプラットフォームでのみ実行
      if (Capacitor.isNativePlatform()) {
        try {
          await Purchases.setLogLevel({ level: LOG_LEVEL.DEBUG });

          // Firebase Authの状態復元を待つ
          onAuthStateChanged(this.authService.afAuth, async (user) => {
            if (user) {
              await Purchases.configure({
                apiKey: 'APIキー',
                appUserID: user.uid,
              });
            } else {
              await Purchases.configure({
                apiKey: 'APIキー',
              });
            }
            AppComponent.purchasesConfigured = true;
          });
        } catch (error) {
          console.error('RevenueCat configuration failed:', error);
        }
      } else {
        // ウェブプラットフォームの場合
        AppComponent.purchasesConfigured = true;
      }
    });
  }
}

apiKeyには、RevenueCatのPublic APIキー、appUserIDには、Firebase AuthenticationのユーザーUID(user.uid)が入ります。

サブスクリプション管理プラットフォームRevenueCatのAPI keys設定画面のスクリーンショットです。画面は主に2つのセクションに分かれています。「Secret API keys」のセクションには、「Generate secret API keys to access additional API functions. Secret API keys should never be exposed to the public, such as front-end code or GitHub.」という説明文と、「+ New secret API key」ボタン、そしてラベル、Secret API key、API Version、Created、Actionsの列ヘッダーが表示されていますが、現在Secret APIキーは登録されていません。「SDK API keys」のセクションには、「These are the API keys you'll use to configure the RevenueCat SDK. Public API keys are automatically generated for each of your apps.」という説明文があり、アプリ名(「Kanau -「つくる人」のための計算メモ帳 (App Store)」)、Public API key(「Show key」と表示され赤枠で囲まれている)、Createdの日付(Jun 08, 2025)が表示されています。
RevenueCatのPublic APIキー

また、RevenueCatのCapacitorプラグインはiOSでのみ利用したいため、ネイティブプラットフォームでのみ実行するように記述しています。

2. 購入処理を実装する

プラットフォーム判定や初期化チェック、購入後のFirebaseでのユーザー情報更新などを処理を書きます。
今回は、課金できるのはiOSのみのため、iOSでのみ購入処理を続行できるようにしています。

// settings.page.ts
async purchaseSearchAddon() {
  // プラットフォーム制限(iOS Only)
  if (!this.platform.is('ios')) {
    alert(this.translate.instant('PurchaseNotSupportedPlatform'));
    return;
  }

  // 初期化状態確認
  if (!AppComponent.purchasesConfigured) {
    alert('課金機能の初期化中です。しばらくしてから再度お試しください。');
    return;
  }

  try {
    // Offeringsから商品を取得
    const offerings = await Purchases.getOfferings();
    const currentOffering = offerings.current;

    if (!currentOffering) {
      throw new Error('Offeringが見つかりません');
    }

    // カスタムパッケージを検索
    const packageToBuy = currentOffering.availablePackages.find(
      (pkg) => pkg.identifier === 'custom'
    );

    if (!packageToBuy) {
      throw new Error('購入パッケージが見つかりません');
    }

    // 購入実行
    await Purchases.purchasePackage({ aPackage: packageToBuy });

    // 購入後のEntitlement確認
    const customerInfo = await Purchases.getCustomerInfo();
    const entitlement = customerInfo.customerInfo.entitlements.active['Search Add-on'];

    if (entitlement) {
      // Firestoreのユーザー情報を更新
      this.user.isPaid = true;
      this.user.paidUntil = entitlement.expirationDate ?? '';
      await this.firestore.userSet(this.user);

      // UIに即座に反映
      const updatedUser = await this.firestore.userInit(this.uid);
      if (updatedUser) {
        this.user = updatedUser;
        this.cdr.detectChanges();
      }
    } else {
      alert(this.translate.instant('PurchaseActivatedFail'));
    }
  } catch (e: any) {
    alert(this.translate.instant('PurchaseFailed', { error: e.message || e }));
  }
}

3. 購入復元処理を実装する

次に、ユーザーがアプリ再インストールを行った場合などに備えて、購入状況を復元できる処理も実装します。

async restoreSearchAddon() {
  // プラットフォーム制限(iOS)
  if (!(this.platform.is('ios'))) {
    alert(this.translate.instant('PurchaseNotSupportedPlatform'));
    return;
  }

  if (!AppComponent.purchasesConfigured) {
    alert('課金機能の初期化中です。しばらくしてから再度お試しください。');
    return;
  }

  try {
    await Purchases.restorePurchases();
    const customerInfo = await Purchases.getCustomerInfo();
    const entitlement = customerInfo.customerInfo.entitlements.active['Search Add-on'];

    if (entitlement) {
      this.user.isPaid = true;
      this.user.paidUntil = entitlement.expirationDate ?? '';
      await this.firestore.userSet(this.user);

      alert(this.translate.instant('RestoreSuccess'));

      // UIに即座に反映
      const updatedUser = await this.firestore.userInit(this.uid);
      if (updatedUser) {
        this.user = updatedUser;
        this.cdr.detectChanges();
      }
    } else {
      alert(this.translate.instant('RestoreNoInfo'));
    }
  } catch (e: any) {
    alert(this.translate.instant('RestoreFailed', { error: e.message || e }));
  }
}

4. 有料ユーザーの判定ロジックを作成する

Firestoreのユーザーデータから有料ユーザーかどうかを判定できる、共通の関数を作成します。
アプリからこの関数を呼び出すことで、有料ユーザーかどうかを判定し、アプリで有料機能の表示・非表示を切り替えます。

このアプリでは、開発者が、デバッグのために課金機能をUIからオンオフできるようにしたいため、開発者デバッグ用の上書き処理も実装しました。

// src/app/shared/utils.ts
export function isPaidUser(user: IUser | undefined | null): boolean {
  if (!user) return false;

  // 開発者デバッグ用オーバーライド
  if (user.isDev && user.debugPaidOverride !== undefined) {
    return !!user.debugPaidOverride;
  }

  // サブスクリプション or 買い切り型
  if (user.isPaid) {
    if (!user.paidUntil) return true; // 買い切り型
    if (new Date(user.paidUntil).getTime() > Date.now()) return true; // 有効なサブスクリプション
  }

  return false;
}

ユーザーインターフェース定義:

// src/app/interfaces/user.ts
export interface IUser {
  userId: string;
  currency: string;
  isPaid?: boolean; // 有料ユーザーフラグ
  paidUntil?: string; // サブスクリプション期限(ISO8601形式)
  isDev?: boolean; // 開発者フラグ
  debugPaidOverride?: boolean; // 開発者用デバッグオーバーライド
  photoDataUrl?: string; // プロフィール画像URL
  displayName?: string; // 公開表示名
}

ペイウォール(案内画面)の実装

アプリのユーザー向けに、有料機能がどのようなものなのかを案内する画面も実装しました。
RevenueCatにもペイウォールを作成できる機能があるようなのですが、今回の実装時点では Caparitorプラグインでは利用できなかっため、自力で実装しました。

App Storeの審査について

アプリがApp Storeの審査を通過するためには、サブスクリプションの案内画面もAppleのガイドラインに沿って実装する必要があり、下記の項目をペイウォールに表示する必要がありました。

  • サブスクリプションの名称
  • サブスクリプション期間
  • 価格(各国通貨での動的表示)
  • アプリのプライバシーポリシーへのリンク
  • アプリの利用規約リンク

このうち、各国通貨での動的表示に関してはRevenueCatプラグインを利用すれば可能なはずだったのですが、うまく値を取得できなかったため、日本語では日本円、英語では米ドルで価格をハードコーディングしましたが、それでも審査は通りました。
本来であれば、ログインしているユーザーの国に応じて価格を表示すべきです。

有料ユーザー向けの機能を実装する

課金機能が実装できたら、次は、課金することで有効になる機能を実装していきます。

今回は、「プロジェクト内のアイテムを検索できる機能」と「プロジェクトをウェブページとして公開できる機能」の2つを実装しました。

先の手順で作成した isPaidUser の判定処理を利用して、課金ユーザーには検索UIを表示したり、プロジェクトを公開できるUIを表示したりします。
プロジェクトの公開機能に関しては、サブスクリプションが切れた場合には公開プロジェクトが非表示になるような処理も実装しました。

実機で動作を確認する

機能が実装できたら、課金が期待通りに機能するか、下記の流れで動作を確認しました。
購入、購入の復元、解約まで、全ての過程をテストします。

1. App Store Connectでサンドボックステストユーザーを作成

2. 実機でのテスト準備

  • Xcodeから実機(iPhone/iPad)にビルド
  • App Store公開前でも開発用ビルドでテスト可能

3. 実機でサンドボックスアカウントにサインイン

  • 設定 → Apple ID → メディアと購入 → サインアウト
  • アプリ内課金時に「サンドボックスアカウント」でサインイン

4. 課金フローのテスト

  • 設定画面 → 有料機能セクション → 購入ボタンタップ
  • Appleの課金ダイアログ表示を確認
  • サンドボックスアカウントで購入する
  • Firestoreのユーザー情報(isPaid/paidUntil)が更新されるかを確認
  • 有料機能が即座にON/OFFされるか確認

5. 復元・解約テスト

  • 「購入の復元」ボタンで復元できるか確認する
  • App Store「サブスクリプション管理」から解約
  • 期限切れ後に、有料機能がOFFになっているかを確認

6. RevenueCatダッシュボード確認

  • 「Customers」タブでテストユーザーの購入履歴確認
  • Entitlementの状態確認

テストの際は、自分のApple IDで試すのではなく、テスト用のサンドボックスユーザーを作成する必要があります。
また、iOSシミュレータでは課金のテストはできないため、必ず実機での動作確認が必要になります。

課金周りのテストが実機でしか行えないのは、かなり不便に感じました。
自分が普段から利用しているiPhoneで動作を確認したのですが、サンドボックスユーザーでログインするためには、自分のApple IDからログアウトする必要がありました。ログアウトしてしまうと、原則として、Apple IDに紐づくiCloudなどのすべての情報が端末から削除されるため、大掛かりです。
できれば、テスト専用の端末を持ちたいものだと思いました。
ウェブブラウザ上で完結するアプリとは異なり、Playwrightなどで自動テストが行えない点も不便だと感じました。

サブスクリプション期限が切れた後に有料機能が無効化されるかのテストについては、RevenueCat側でサンドボックスユーザーを判定しているようで、通常一ヶ月単位のサブスクリプションが、かなり短い時間で切れるようになっており、テストしやすかったです。

まとめ

Ionic/Angular + RevenueCat + Firebaseの組み合わせで、iOS/ウェブ両対応のサブスクリプション課金システムを構築できました。

今回の実装で最も重要だったのは、Apple App Store ガイドラインへの対応です。
当初、プライバシーポリシーや利用規約は、サブスクリプションの案内画面ではなく他のページから案内しているので、それで良いと考えていたのですが、案内画面内に表示しないと審査が通りませんでした。
価格表示に関しても、「使ってみる(購入する)」ボタンをタップすれば、その先で、ユーザーの国に応じた表示が出るのでそれで良いだろうと考えていましたが、事前にわかりやすい表示が必要でした。

個人開発や小規模チームでも、RevenueCatの無料枠を活用することで、クロスプラットフォームの課金機能を比較的簡単に実装できるのは大きなメリットだと感じました。

明日の Advent Calendar記事は、ディレクター田口さんによる「【シャーメゾン】ブランドサイトと物件検索サイトを兼ね備えた大規模リニューアルの裏側」です!どうぞお楽しみに🎄


📮 お仕事のご依頼やご相談、お待ちしております。

お仕事のご依頼やご相談は、お問い合わせ からお願いいたします。

🤝 一緒に働きませんか?

下記の職種を募集中です。より良いデザイン、言葉、エンジニアリングをチームで追求していける方をお待ちしております。詳細は 採用情報 をご覧ください。

  • アシスタントデザイナー
  • フロントエンドエンジニア
  • アシスタントフロントエンドエンジニア

🗒 会社案内資料もご活用ください。

会社のサービスや制作・活動実績、会社概要、ご契約など各種情報をまとめた資料をご用意しています。会社案内資料 からダウンロード可能ですので、ぜひご活用ください。

2025年12月3日更新

株式会社necco ダウンロード資料へのバナー画像