この記事は、neccoのアドベントカレンダー2025 3日目の記事です!
2025年7月、App Storeに公開している個人開発のiOSアプリ「Kanau」に、RevenueCatを使って有料サブスクリプション機能を実装しました。 そこで、RevenueCatを使って有料サブスクリプションを追加した際の手順をご紹介します。
アプリの概要

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つの工程が必要です。
- App Store Connectで有料機能を商品として追加する
- アプリ内で商品の購入や復元処理を実装する
私のアプリは、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でアカウントを作り、その後に具体的な実装を行い、テストを行い、公開します。
- RevenueCatアカウント開設
- App Store Connectで商品登録
- RevenueCat上で商品設定
- purchases-capacitorプラグイン導入
- サブスクリプション機能実装
- サンドボックスアカウントを作成
- 実機テスト
- 公開申請
実装前の準備
App Store Connectでの準備
まずは、App Store Connectで、商品(サブスクリプション)を登録します。
App Store Connectでアプリを開き、Monetizationタブの「Subscriptions」を開きます。 Subscription Groupsの横の「+」ボタンをクリックし、サブスクリプショングループを作成します。

グループの中に、サブスクリプションを作成します。
表示されている登録項目に沿って、サブスクリプションの名称や価格、期間、ローカライゼーション情報、そして審査用の機能のスクリーンショットを追加し、保存します。
アプリを全世界で公開する場合、価格を一つ一つ設定する必要があるのですが、基準とする価格から自動的に他の国の通貨に換算してくれる機能がついており、それほど手間なく設定できました。換算後に、自分で価格を調整することも可能でした。

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

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

Xcodeで課金設定を有効化する
XcodeでiOSプロジェクトを開き、「+Capability」をクリックして、「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の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でサンドボックステストユーザーを作成
- Apple公式:サンドボックスアカウント作成方法
- 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日更新


