RevenueCat 完全セットアップガイド


1. 共通の前提設定(Dashboard)

まずRevenueCat Dashboard側で行う共通作業です。

Products → Entitlements → Offerings の関係

Product(ストア側のID)
  └── Entitlement(アクセス権の抽象化)
        └── Offering(表示パッケージのまとまり)
              └── Package(monthly/annual/lifetime等)

Dashboard手順

  1. Project作成 → iOS App / Android App をそれぞれ追加
  2. Products に各ストアのProduct IDを登録
  3. Entitlements を作成(例: premium)→ Productsをアタッチ
  4. Offerings を作成 → Packages に Products を紐付け

2. iOS設定

App Store Connect側

項目サブスク買い切り
種別Auto-Renewable SubscriptionNon-Consumable(消耗しない)
グループSubscription Group必須不要
無料トライアル設定可能不可

App Store Connect手順(サブスク)

  1. App Store Connect → アプリ → サブスクリプション → グループ作成
  2. サブスクリプション追加 → Product ID設定(例: com.yourapp.premium.monthly
  3. 価格・期間・説明を設定してレビュー提出

RevenueCat Dashboard(iOS)

  • iOS AppBundle ID を入力
  • App Store Connect API Key を登録(In-App Purchase Key、非共有キーを推奨)

iOSコード実装

// AppDelegate or @main
import RevenueCat

func application(...) -> Bool {
    Purchases.logLevel = .debug
    Purchases.configure(withAPIKey: "appl_XXXXXXXXXX")
    // ログインユーザーがいる場合
    // Purchases.configure(withAPIKey: "...", appUserID: userId)
    return true
}
// 購入画面
func loadOfferings() async {
    do {
        let offerings = try await Purchases.shared.offerings()
        guard let current = offerings.current else { return }
        // current.availablePackages でパッケージ一覧取得
    } catch {
        print(error)
    }
}

// 購入実行(サブスク・買い切り共通)
func purchase(package: Package) async {
    do {
        let result = try await Purchases.shared.purchase(package: package)
        if result.customerInfo.entitlements["premium"]?.isActive == true {
            // アクセス付与
        }
    } catch {
        print(error)
    }
}

// リストア(買い切りでは必須UIとしてAppleが要求)
func restorePurchases() async {
    do {
        let customerInfo = try await Purchases.shared.restorePurchases()
        // entitlementsを確認
    } catch { }
}

3. Android設定

Google Play Console側

項目サブスク買い切り
種別SubscriptionsOne-time products(INAPP)
Base Plan必須(monthly等)不要
Offer無料トライアル等不要

Google Play Console手順(サブスク)

  1. 収益化サブスクリプション → 作成
  2. Product ID設定(例: premium_monthly
  3. Base Plan追加 → 期間・価格設定
  4. Offer(任意)でトライアル設定

RevenueCat Dashboard(Android)

  • Google Play AppPackage Name を入力
  • Service Account JSONをアップロード(Google Play → API Access → サービスアカウント作成 → 財務管理者権限付与)

Androidコード実装

// Application.onCreate()
import com.revenuecat.purchases.Purchases
import com.revenuecat.purchases.PurchasesConfiguration

Purchases.debugLogsEnabled = true
Purchases.configure(
    PurchasesConfiguration.Builder(context, "goog_XXXXXXXXXX").build()
)
// Offerings取得
Purchases.sharedInstance.getOfferingsWith(
    onError = { error -> },
    onSuccess = { offerings ->
        val packages = offerings.current?.availablePackages ?: return@getOfferingsWith
        // UI表示
    }
)

// 購入実行(Activity内)
Purchases.sharedInstance.purchaseWith(
    PurchaseParams.Builder(activity, packageToPurchase).build(),
    onError = { error, userCancelled -> },
    onSuccess = { storeTransaction, customerInfo ->
        if (customerInfo.entitlements["premium"]?.isActive == true) {
            // アクセス付与
        }
    }
)

4. サブスク vs 買い切り の実装上の違い

観点サブスク買い切り(Non-Consumable)
Entitlement有効期間更新毎に延長永続
リストア自動(レシート確認)必須ボタンをUI提供
AndroidキャンセルcancelledDateが入る基本なし
WebhookINITIAL_PURCHASE, RENEWAL, CANCELLATION 等INITIAL_PURCHASE のみ

5. テスト方法

iOS テスト

Xcode → Product → Scheme → Edit Scheme → StoreKit Configuration
→ .storekit ファイルを作成してローカルテスト

または Sandbox環境

  1. App Store Connect → ユーザーとアクセスSandboxテスター でアカウント作成
  2. 実機の設定 → App Store → Sandboxアカウントでサインイン
  3. サブスクの更新間隔は短縮される(1ヶ月→5分等)
// Sandboxか確認
Purchases.shared.getCustomerInfo { info, error in
    print(info?.originalAppUserId)
}

Android テスト

  1. License Tester:Google Play Console → 設定ライセンステスター にGoogleアカウント追加
  2. Test Track:Internal Testingにアップロード必須(実際のPlay Billingはリリース済みアプリが必要)

RevenueCat Dashboard でのテスト確認

  • Customers タブ → App User IDで検索 → トランザクション履歴確認
  • Sandbox データと Production データが切り替え表示可能

6. Webhook設定 → Firebase連携

RevenueCat Dashboard設定

Project SettingsIntegrationsWebhooks → URL登録

https://asia-northeast1-YOUR_PROJECT.cloudfunctions.net/revenuecatWebhook

Cloud Functions 実装全体像

// functions/src/revenuecatWebhook.ts
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
import * as sgMail from "@sendgrid/mail";

admin.initializeApp();
const db = admin.firestore();
sgMail.setApiKey(process.env.SENDGRID_API_KEY!);

export const revenuecatWebhook = functions
  .region("asia-northeast1")
  .https.onRequest(async (req, res) => {
    
    // 1. 認証(RevenueCat Authorization Header)
    const authHeader = req.headers.authorization;
    if (authHeader !== process.env.REVENUECAT_WEBHOOK_SECRET) {
      functions.logger.warn("Unauthorized webhook attempt");
      res.status(401).send("Unauthorized");
      return;
    }

    const event = req.body;
    const { event: eventData } = event;

    try {
      // 2. Firestoreに格納
      await savePurchaseHistory(eventData);

      // 3. メール送信(特定イベントのみ)
      await sendEmailIfNeeded(eventData);

      // 4. ログ書き込み
      logEvent(eventData);

      res.status(200).send("OK");
    } catch (error) {
      functions.logger.error("Webhook processing error", { error, eventData });
      res.status(500).send("Internal Server Error");
    }
  });
// ── Firestore格納 ──────────────────────────────
async function savePurchaseHistory(event: any) {
  const userId = event.app_user_id;
  const eventType = event.type; // INITIAL_PURCHASE, RENEWAL, CANCELLATION, etc.

  const record = {
    eventType,
    productId: event.product_id,
    platform: event.store,               // APP_STORE / PLAY_STORE
    price: event.price ?? null,
    currency: event.currency ?? null,
    purchaseDate: event.purchased_at_ms
      ? admin.firestore.Timestamp.fromMillis(event.purchased_at_ms)
      : null,
    expirationDate: event.expiration_at_ms
      ? admin.firestore.Timestamp.fromMillis(event.expiration_at_ms)
      : null,
    cancelledDate: event.cancel_reason ?? null,
    environment: event.environment,      // SANDBOX / PRODUCTION
    transactionId: event.transaction_id,
    createdAt: admin.firestore.FieldValue.serverTimestamp(),
    rawEvent: event,                     // 生データも保存しておくと安心
  };

  // コレクション構造: users/{userId}/purchaseHistory/{transactionId}
  await db
    .collection("users")
    .doc(userId)
    .collection("purchaseHistory")
    .doc(event.transaction_id ?? db.collection("_").doc().id)
    .set(record, { merge: true }); // idempotent

  // サマリーも users/{userId} に最新状態を反映
  await db.collection("users").doc(userId).set(
    {
      subscriptionStatus: deriveStatus(event),
      lastEventType: eventType,
      lastEventAt: admin.firestore.FieldValue.serverTimestamp(),
    },
    { merge: true }
  );
}
// ── メール送信 ─────────────────────────────────
const EMAIL_TRIGGER_EVENTS = new Set([
  "INITIAL_PURCHASE",
  "RENEWAL",
  "CANCELLATION",
  "EXPIRATION",
  "BILLING_ISSUE",
]);

async function sendEmailIfNeeded(event: any) {
  if (!EMAIL_TRIGGER_EVENTS.has(event.type)) return;

  const userId = event.app_user_id;
  const userDoc = await db.collection("users").doc(userId).get();
  const email = userDoc.data()?.email;
  if (!email) return;

  const subjects: Record<string, string> = {
    INITIAL_PURCHASE: "ご購入ありがとうございます",
    RENEWAL:          "サブスクリプションが更新されました",
    CANCELLATION:     "サブスクリプションのキャンセルを受け付けました",
    EXPIRATION:       "サブスクリプションが終了しました",
    BILLING_ISSUE:    "【要確認】お支払いに問題が発生しました",
  };

  await sgMail.send({
    to: email,
    from: "noreply@yourapp.com",
    subject: subjects[event.type] ?? "購入状況のお知らせ",
    text: buildEmailBody(event),
  });
}
// ── Cloud Functionsログ ────────────────────────
function logEvent(event: any) {
  functions.logger.info("RevenueCat event received", {
    eventType: event.type,
    userId: event.app_user_id,
    productId: event.product_id,
    environment: event.environment,
    store: event.store,
    transactionId: event.transaction_id,
  });
}

// ── ステータス導出ヘルパー ─────────────────────
function deriveStatus(event: any): string {
  const map: Record<string, string> = {
    INITIAL_PURCHASE: "active",
    RENEWAL:          "active",
    UNCANCELLATION:   "active",
    CANCELLATION:     "cancelled",
    EXPIRATION:       "expired",
    BILLING_ISSUE:    "billing_issue",
  };
  return map[event.type] ?? "unknown";
}

主要なWebhookイベント一覧

イベント発生タイミング
INITIAL_PURCHASE初回購入(サブスク・買い切り共通)
RENEWALサブスク自動更新
CANCELLATIONユーザーがキャンセル操作
EXPIRATIONサブスク期限切れ
BILLING_ISSUE支払い失敗
UNCANCELLATIONキャンセル撤回
PRODUCT_CHANGEプラン変更
TRANSFERユーザーID移転

7. セキュリティチェックリスト

// Webhook Secret の設定
// RevenueCat Dashboard → Project Settings → Webhooks
// → Authorization Header に任意の文字列を設定

// Firebase側:environment config or Secret Manager
// firebase functions:secrets:set REVENUECAT_WEBHOOK_SECRET
  • transaction_id をドキュメントIDにして 冪等性を確保
  • environment: SANDBOX のイベントは本番Firestoreに書かないようフィルタリング推奨
  • ✅ RevenueCat SDKではなくサーバー側(Webhook)を信頼の根拠にする

特に詰まりやすい箇所(Google PlayのService Account権限、Sandboxテスターの設定、Webhookの冪等処理)があれば、そこだけ深掘りします!