> ## Documentation Index
> Fetch the complete documentation index at: https://docs.theacornkit.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Switch to RevenueCat in one line

> Swap the default StoreKit 2 provider for RevenueCat without touching a screen.

Two parts: **A.** one-time RevenueCat dashboard config; **B.** the one-line app swap.
For **whether** you want this (short version: only for the dashboard or cross-platform
entitlements), see the StoreKit-vs-RevenueCat note shipped in the kit.

<Note>
  **Architecture recap:** `SubscriptionProviding` is the seam (in `AcornCore`). The
  default `StoreKitSubscriptionProvider` (StoreKit 2, zero dependencies) ships in
  core; `RevenueCatSubscriptionProvider` ships in the optional `AcornCoreRevenueCat`
  module. The flow gate (`AppFlowCoordinator` + `AcornGateView`), every paywall
  (`PlanListPaywall` / `SingleCTAPaywall`), and the Settings status card talk to the
  protocol — `any SubscriptionProviding` — so they are **unchanged** by the swap.
</Note>

## Part A — RevenueCat setup (one-time, \~20 min)

### A1. Create the project + get the API key

1. app.revenuecat.com → **+ New Project**. Add an **App** under it (Project Settings → Apps → **+ App Store**), bind it to your App Store Connect app, and upload your App Store Connect **In-App Purchase Key** (.p8) so RevenueCat can read transactions.
2. Copy the **public SDK key** for the Apple app (Project Settings → API Keys → "App-specific" / "Public" — starts with `appl_`). This is the only value the app needs. It's a **public** key, safe to ship in the client — never ship a **secret** API key.

### A2. Create the entitlement

Project → **Entitlements** → **+ New** → identifier **`pro`**. This is the single entitlement that unlocks the app. (Override the id in the initializer if you name it differently — see Step 2.)

### A3. Create products + an offering mapped to the four plans

The provider maps RevenueCat **package types** to AcornCore's `SubscriptionPlan`:

| AcornCore `SubscriptionPlan` | RevenueCat package type |
| ---------------------------- | ----------------------- |
| `.weekly`                    | Weekly                  |
| `.monthly`                   | Monthly                 |
| `.annual`                    | Annual                  |
| `.lifetime`                  | Lifetime                |

1. **Products** → import / add your App Store Connect products. Use the same id convention as the StoreKit path — `<prefix>.{weekly,monthly,yearly,lifetime}` — so the tier resolves exactly (see Step 2's `productIDPrefix`).
2. **Offerings** → make a **current** offering and attach a package per plan you sell, each pointing at the matching product. The provider reads prices from the current offering's packages and attaches each package's entitlement on purchase.

<Tip>
  Only ship plans you actually sell. A package absent from the offering just means
  `displayPrice(for:)` returns a placeholder for that plan — wire your paywall to the
  plans you offer.
</Tip>

## Part B — App swap (one line)

This is the whole point of the protocol seam: **only the composition root changes.**
The paywall, the gate, and Settings are typed against `any SubscriptionProviding`, so
they compile and run untouched.

<Steps>
  <Step title="Add the dependency">
    The app already depends on `AcornCore`. Add the **`AcornCoreRevenueCat`** product to
    the app target (it pulls `purchases-ios`, ≥ 5.0). Local-package apps just add the
    product; SPM apps get `https://github.com/RevenueCat/purchases-ios.git` transitively
    via the module. Core stays dependency-free — only apps that opt in pull the SDK.
  </Step>

  <Step title="Swap the provider in AppConfig.makeSubscriptions()">
    `makeSubscriptions()` already returns `any SubscriptionProviding`. Change the body
    from the StoreKit provider to the RevenueCat one — nothing downstream changes:

    ```swift theme={null}
    // Before (default — StoreKit 2, no dependency, no revenue share):
    @MainActor
    static func makeSubscriptions() -> any SubscriptionProviding {
        StoreKitSubscriptionProvider(
            productIDPrefix: productIDPrefix,
            appGroupIdentifier: appGroupIdentifier
        )
    }

    // After (RevenueCat):
    @MainActor
    static func makeSubscriptions() -> any SubscriptionProviding {
        RevenueCatSubscriptionProvider(
            apiKey: revenueCatKey,
            // entitlementID: "pro",          // default; override only if renamed
            productIDPrefix: productIDPrefix    // optional: exact tier mapping
        )
    }
    ```

    Add `import AcornCoreRevenueCat` at the top of `AppConfig.swift`. That, plus the SPM
    product in Step 1, is the entire code change.
  </Step>

  <Step title="Supply the key (keep it out of source)">
    Mirror the PostHog key pattern — read it from Info.plist so it isn't hardcoded:

    ```swift theme={null}
    /// RevenueCat public SDK key. Read from Info.plist (`REVENUECAT_API_KEY`).
    static var revenueCatKey: String {
        Bundle.main.object(forInfoDictionaryKey: "REVENUECAT_API_KEY") as? String ?? ""
    }
    ```

    Set `REVENUECAT_API_KEY` in build settings / Info.plist (the public `appl_…` key is
    safe to ship). That's it — run the app; the paywall now shows RevenueCat prices and
    purchases flow through RevenueCat.
  </Step>
</Steps>

### Optional — tie entitlements to a signed-in account

If you also ship auth ([Auth setup](/auth-setup)), pass the user id so RevenueCat entitlements follow the
account across devices/reinstalls:

```swift theme={null}
RevenueCatSubscriptionProvider(apiKey: revenueCatKey,
                               productIDPrefix: productIDPrefix,
                               appUserID: auth.currentUser?.id)   // omit for anonymous ids
```

Re-create the provider (or call RevenueCat's `logIn`) after sign-in so the id aligns.

## Why this is genuinely one line

Every consumer of the provider — `AppShell`, `PlanListPaywall`, `SingleCTAPaywall`,
`PaywallContent`, `RootTabView`, `HomeView`, `SettingsView` — declares
`subscriptions: any SubscriptionProviding`, and the gate (`AppFlowCoordinator`) always
took the protocol. No call site references a StoreKit or RevenueCat type. So swapping
the return value in `makeSubscriptions()` is the only code edit; nothing else can break.
This is the protocol-seam selling point in action: **choice, not lock-in.**

## Pre-ship checklist

* [ ] RevenueCat project + Apple app bound to App Store Connect; IAP key uploaded.
* [ ] `pro` entitlement created (or id overridden in the initializer).
* [ ] Current offering has a package per plan you sell, mapped to the right products.
* [ ] `AcornCoreRevenueCat` product added to the app target.
* [ ] `makeSubscriptions()` returns `RevenueCatSubscriptionProvider`; `REVENUECAT_API_KEY` set in Info.plist (public key, not a secret key).
* [ ] **Live test in simulator/device:** prices load, purchase a plan, the gate unlocks `paywall → main`, restore purchases works, Settings shows the active tier.
* [ ] If using auth: `appUserID` set to the account id after sign-in; entitlements follow the account.

<Warning>
  **What `swift test` can't cover** — verify these on a real simulator/device:

  * Prices, purchases, and entitlement resolution require a real RevenueCat project + StoreKit sandbox (or a `.storekit` config) — `swift test` only covers the plan↔package / tier-mapping logic against mocks.
  * The public SDK key must match the bound Apple app, or `loadProducts()` returns no offerings.
  * `SubscriptionError` is **not** `Equatable` — handle cancellation with `catch SubscriptionError.userCancelled`, not `== .userCancelled`.
</Warning>
