# Set up RevenueCat

Guide to enable subscriptions and in-app purchases after the CLI generated the project.

---

## What the CLI already did for you

| Done | Still on you |
|------|--------------|
| Installed `purchases_flutter` | Create a RevenueCat account |
| Configured the keys in `.env` (test/iOS prod/Android prod) | Create Products, Entitlements and Offerings in the RC dashboard |
| Generated the paywall and subscriptions repository code | Register the webhook URL in the RC dashboard |
| Firebase: deployed the webhook Cloud Function | — |
| Supabase: deployed the webhook Edge Function | — |

> Keys live in `.env` at the project root (source of truth) and are mirrored in `.vscode/launch.json` + `Makefile`. All of them are in `.gitignore` — they never reach the repository.

### Which key to use?

The CLI asks for **three optional keys** (at least one is required):

| Variable | Prefix | Use |
|---|---|---|
| `RC_TEST_KEY` | `test_` | Test Store. **A single key**, works for iOS+Android. Auto-used on simulator/emulator. |
| `RC_IOS_PROD_KEY` | `appl_` | App Store (Sandbox + Production). Auto-used on physical iPhone. |
| `RC_ANDROID_PROD_KEY` | `goog_` | Google Play (Sandbox + Production). Auto-used on physical Android. |

`kasy run` picks the right key based on the device. Force manually: `kasy run --rc=test` or `kasy run --rc=prod`.

---

## Step 1 — Create the RevenueCat account and project

1. Go to [app.revenuecat.com](https://app.revenuecat.com) → create a free account
2. Create a project → give it a name (e.g. your app's name)

---

## Step 2 — Create the app in RevenueCat

Still inside the project:

**To get started (Test Store — no Apple/Google account)**

Project → Apps → **+ Add app** → select **Test Store** → copy the `test_xxx` key.

Paste that same key when the CLI asks for both iOS and Android (or update the files manually if the project already existed).

**For production**

Create a separate app per platform:
- **App Store** → key starts with `appl_`
- **Google Play** → key starts with `goog_`

> ⚠️ **Critical rule:** the app type and the key prefix must match. `Test Store` → `test_`, `App Store` → `appl_`, `Google Play` → `goog_`. Using the wrong key causes `INVALID_CREDENTIALS`.

---

## Step 3 — Create Products, Entitlements and Offerings

You can do this in the dashboard or ask Claude with the RevenueCat MCP ("Create a product `premium_monthly`, entitlement `premium_access` and offering `default`").

**In the RC dashboard** (same URL for every project):

1. [app.revenuecat.com](https://app.revenuecat.com) → your project → **Products** → `+ New`
   - Create the plans (e.g. `premium_monthly`, `premium_annual`)
   - The IDs must be **identical** to the ones you will create in the App Store / Google Play

2. **Entitlements** → `+ New`
   - Create `premium_access` → click the entitlement → **Attach** → select the products

3. **Offerings** → `+ New`
   - Create `default` → open the offering → `+ Add package` → select the products

> **About product IDs:** they must match character by character. `premium_monthly` in the App Store and `premium_monthly` in RC — any difference and the product does not show up in the paywall.

---

## Step 4 — Configure the webhook

The webhook keeps your database's `subscriptions` table updated on every purchase, renewal or cancellation.

**The function URL was already deployed by the CLI. You only need to register it in RevenueCat.**

Find the function URL:

| Backend | Where to find the URL |
|---------|-----------------------|
| **Supabase** | `https://YOUR_PROJECT_REF.supabase.co/functions/v1/revenuecat-webhook` |
| **Firebase** | [Firebase Console → Functions](https://console.firebase.google.com/project/_/functions) → `subscriptions-revenuecatWebhook` → copy the URL |

Register it in RevenueCat:

1. [app.revenuecat.com](https://app.revenuecat.com) → your project → **Integrations** → **Webhooks** → `+ Add webhook`
2. Fill in:

| Field | What to enter |
|-------|---------------|
| **Webhook name** | Any name (e.g. `firebase` or `supabase`) |
| **Webhook URL** | The function URL above |
| **Authorization header value** | `Bearer ` + the value of `REVENUECAT_WEBHOOK_KEY` (e.g. `Bearer rc_wh_abc123`) |
| **Environment** | `Both Production and Sandbox` |
| **Events filter** | `All apps` / `All events` |

3. Click **Send test event** — if it returns `200 OK`, it's working.

> ⚠️ **Authorization header:** the field must contain `Bearer ` followed by your token — including the space. The function rejects any header that does not follow this format.

---

## Step 5 — iOS: configure in App Store Connect

Needed once you leave the Test Store and want to test real purchases on an iPhone.

**Cost:** USD $99/year (Apple Developer Program)

Follow this exact order — skipping any step makes the products not show up.

**1. Create the Apple Developer account**

[developer.apple.com](https://developer.apple.com) → create the account and pay the USD $99/year.

**2. Set up the business side — required for Sandbox to work**

> ⚠️ **This is the most skipped and most blocking step.** Without it, Sandbox returns an empty product list even if you did everything else right: RC account, product created, Sandbox Tester on the iPhone. Nothing works.

[appstoreconnect.apple.com](https://appstoreconnect.apple.com) → top menu → **Agreements, Tax, and Banking**:

- Accept the **Paid Applications Agreement**
- Register the **bank account** where you will get paid
- Fill in the **tax forms** (country, individual or company)
- Accept the required **compliance** items
- If selling to Europe: fill in the additional public information required

After completing everything, **wait 4 to 6 hours** for the settings to propagate. Only then does Sandbox start working correctly. Trying earlier returns an empty list or generic errors.

**3. Create the app in App Store Connect**

[appstoreconnect.apple.com](https://appstoreconnect.apple.com) → **My Apps** → `+` → **New App** → use the same Bundle ID as the Flutter project.

**4. Create the subscriptions**

App Store Connect → your app → **Monetization** → **Subscriptions**:

a) **Create the subscription group** (e.g. "Premium") — every subscription lives inside a group.

b) **Set the grace period** (optional but recommended): click **Grace Period** → **Edit** → choose the duration. Recommended: **3 days** for most apps. It keeps the subscriber's access during temporary billing failures before cancelling.

c) **Create the products** inside the group:
   - Add the products with the same IDs as in RevenueCat (e.g. `subscription_monthly_01`)
   - Fill in: price, duration, subscription language and screenshot

d) **Set the group language** — the step most people miss:

   Inside the group → **Language** section → `+` → select the language (e.g. English US) → fill in the **Group display name** (e.g. `premium`) → save.

   > ⚠️ **Without this step the products get stuck in "Missing Metadata"** and never move to "Prepare for Submission" or "Ready to Submit", even with price, language and subscription screenshot filled in. The **group** language is different from the individual subscription language.

e) **Subscription screenshot** (required for review submission):

   Take a screenshot of your app's paywall running on the iOS simulator or a physical iPhone (`make run-ios` → open the premium screen). Use that image in the **Screenshot** field of each subscription. The **Review notes** field is optional — you can briefly describe the product.

After filling everything in and setting the group language, the status goes from **Missing Metadata** → **Prepare for Submission** → **Ready to Submit**. Either of the last two is fine.

**5. Create the app in RevenueCat as App Store**

[app.revenuecat.com](https://app.revenuecat.com) → your project → **Apps** → `+ Add app` → **App Store** → copy the `appl_xxx` key.

Paste it in the root `.env`:

```env
RC_IOS_PROD_KEY=appl_xxxxxxxxxxxxxxx
```

`kasy run` uses this key automatically on a physical iPhone (the simulator keeps using `RC_TEST_KEY`).

**6. Create a Sandbox Tester — required to test on a physical iPhone**

[appstoreconnect.apple.com](https://appstoreconnect.apple.com) → **Users and Access** → **Sandbox** tab → **Testers** → `+`

Create an e-mail that **has no Apple account associated** (a fresh Gmail works well). The e-mail must be reachable — Apple sends a verification code to confirm the account.

> The Sandbox Tester is **not** a real Apple ID. It's a test-only account that works exclusively in the Sandbox environment. Without it, the iPhone asks for the regular Apple ID and the purchase goes to production.

**7. Enable Developer Mode on the iPhone (iOS 16+)**

Required to run apps straight from Xcode/terminal on a physical device.

iPhone → **Settings** → **Privacy & Security** → scroll to the bottom → **Developer Mode** → enable → the iPhone restarts to confirm.

> If the option is not there, connect the iPhone to the Mac with Xcode open at least once — that unlocks Developer Mode.

**8. Sign in with the Sandbox account on the iPhone**

On modern iPhones (iOS 16+), the Sandbox account lives inside the Developer section:

iPhone → **Settings** → scroll to the bottom → **Developer** → scroll to the bottom of the page → **Sandbox Account** → **Sign In** → use the Sandbox Tester's e-mail and password.

> On older iOS versions the path was Settings → App Store → Sandbox Account. On current iPhones the correct path is through the Developer menu as above.

**9. Configure the P8 key (recommended for sandbox, required for production)**

[appstoreconnect.apple.com](https://appstoreconnect.apple.com) → **Users and Access** → **Integrations** → **In-App Purchase** → `+` → download the `.p8` → paste it in RevenueCat under **App Settings** → **In-App Purchase Key**.

> You can only download the P8 once. Keep it somewhere safe.

**9. Run on the physical iPhone**

```bash
make run-ios
```

Make a purchase — the real Apple modal shows up. With the Sandbox account active, the purchase charges nothing.

---

## Step 6 — Android: configure in Google Play Console

Needed once you leave the Test Store and want to test real purchases on Android.

**Cost:** USD $25 (one-time payment)

**1. Create the Google Play Developer account**

[play.google.com/console](https://play.google.com/console) → create the account and pay the USD $25.

**2. Set up the payments profile**

Google Play Console → **Settings** (main side menu) → **Developer account** → **Payments profile** → fill in legal name, address and tax data.

**3. Create the app**

Google Play Console → **Create app** → fill in name, language and category.

**4. Publish to the Internal testing track — required to create subscriptions**

**Testing** → **Internal testing** → create the release → upload a signed APK/AAB → publish.

> The app does not need to be complete — a signed development build is enough.

**5. Create the subscriptions**

Google Play Console → your app → **Monetize** → **Subscriptions** → `+ Create subscription`:
- Create the products with the same IDs as in RevenueCat
- Leave them in `Active` state

**6. Create the Service Account (RevenueCat's credential for Google)**

a) [console.cloud.google.com](https://console.cloud.google.com) → select the app's project → **APIs & Services** → **Library** → enable:
   - **Google Play Android Developer API**
   - **Google Play Developer Reporting API**

b) **IAM & Admin** → **Service accounts** → **+ Create service account**:
   - Name: `revenuecat-service` (or any name)
   - Roles: **Pub/Sub Editor** + **Monitoring Viewer**
   - Click **Done**

c) Click the new account → **Keys** tab → **Add key** → **Create new key** → **JSON** → download the file.

d) Google Play Console → **Settings** → **Users and permissions** → **Invite new users**:
   - E-mail: the Service Account's e-mail (visible in the Cloud Console)
   - Permissions: **Manage orders and subscriptions** + **Manage financial reports**
   - Save

> After configuring, wait up to 36 hours for the credentials to propagate. 503/521 errors in RC during this window are normal.

**7. Configure RevenueCat with the Google credentials**

[app.revenuecat.com](https://app.revenuecat.com) → your project → **Apps** → `+ Add app` → **Google Play** → upload the JSON file → copy the `goog_xxx` key.

Paste it in the root `.env`:

```env
RC_ANDROID_PROD_KEY=goog_xxxxxxxxxxxxxxx
```

`kasy run` uses this key automatically on a physical Android device (the emulator keeps using `RC_TEST_KEY`).

**8. Add a License Tester — required to test on the device**

Google Play Console → **Settings** → **License testing** → add the e-mail of the Google account signed in on the test Android device.

> Use only **one Google account** on the device — multiple accounts make purchases fail.

**9. Run on the Android device**

```bash
make run-android
```

Make a purchase — the real Google Play modal shows up. In Sandbox, monthly subscriptions renew every 5 minutes.

---

## Quick checklist

### Test Store (no Apple/Google)

- [ ] RevenueCat account and project created
- [ ] App created as **Test Store** — `test_xxx` key in place
- [ ] Products, Entitlements and Offerings configured in RC
- [ ] Webhook registered in RC and tested (`200 OK`)
- [ ] Test purchase activates the entitlement correctly

### iOS (App Store Connect)

- [ ] Apple Developer Program active (USD $99/year)
- [ ] Paid Applications Agreement signed
- [ ] Bank account validated in App Store Connect
- [ ] App created in App Store Connect with the correct Bundle ID
- [ ] Subscription group created with the language set (otherwise products stay in "Missing Metadata")
- [ ] Subscriptions created with IDs identical to RevenueCat, with price, language and screenshot filled in
- [ ] Subscription status at **Ready to Submit**
- [ ] App created in RC as **App Store** — `appl_xxx` key updated in the files
- [ ] Sandbox Tester created in App Store Connect → Users and Access → Sandbox (e-mail with no Apple account)
- [ ] Developer Mode enabled on the iPhone (Settings → Privacy & Security → Developer Mode)
- [ ] Sandbox account signed in on the iPhone (Settings → Developer → Sandbox Account)
- [ ] P8 key configured in RC
- [ ] Purchase tested on a physical iPhone

### Android (Google Play)

- [ ] Google Play Developer active (USD $25)
- [ ] Payments profile configured
- [ ] App created and published to the Internal testing track
- [ ] Subscriptions created with IDs identical to RevenueCat
- [ ] APIs enabled in the Google Cloud Console
- [ ] Service Account created, JSON downloaded and Service Account invited in Google Play
- [ ] Service Account JSON uploaded to RC — `goog_xxx` key updated in the files
- [ ] License Tester added in the Google Play Console
- [ ] Only one Google account signed in on the test device
- [ ] Purchase tested on a physical Android device

---

## Common errors

**`INVALID_CREDENTIALS`** — the app type in RC and the key prefix don't match. `Test Store` requires a `test_` key, `App Store` requires `appl_`, `Google Play` requires `goog_`.

**Products don't show up in the paywall** — product IDs are not identical between the store and RC, or the Paid Applications Agreement is not signed (iOS), or the app is not published to the internal track (Android).

**Sandbox returns an empty list (iOS)** — incomplete business setup in App Store Connect → Agreements, Tax, and Banking. Check: Paid Applications Agreement accepted, bank account registered, tax forms filled in, compliance accepted. After completing everything, wait 4 to 6 hours before testing.

Check with:

```bash
kasy doctor
```

`kasy doctor` shows a **RevenueCat** section automatically when the project uses the feature. Sample output:

```
RevenueCat
  ✓ API keys configured (iOS + Android)
  ⚠ Using Test Store keys (test_) — replace with appl_ and goog_ for production
  ✓ Webhook URL (paste in RevenueCat → Integrations → Webhooks):
     https://YOUR_PROJECT_REF.supabase.co/functions/v1/revenuecat-webhook
```

> For Firebase projects, `kasy doctor` points to where to find the URL in the Firebase Console instead of printing it directly.
