Skip to main content
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.
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.

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 SubscriptionPlanRevenueCat package type
.weeklyWeekly
.monthlyMonthly
.annualAnnual
.lifetimeLifetime
  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.
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.

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.
1

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.
2

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:
// 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.
3

Supply the key (keep it out of source)

Mirror the PostHog key pattern — read it from Info.plist so it isn’t hardcoded:
/// 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.

Optional — tie entitlements to a signed-in account

If you also ship auth (Auth setup), pass the user id so RevenueCat entitlements follow the account across devices/reinstalls:
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.
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.