Revenuecat Appstore Setup

Civix — RevenueCat + App Store Connect Setup (Complete)

The one reference for getting in-app purchases live. Do App Store Connect first, then RevenueCat (RevenueCat needs the ASC product + the P8 key to exist).


0. Your exact values (copy-paste these everywhere)

ThingValue
App nameCivix - US Citizenship Test
Bundle IDcom.civics.test
ASC App ID6759538146
Apple ID (account)mhashimi2019@icloud.com
Apple Team ID22GBQ3S2M7
App version2.1.3
Product ID (the subscription)civix_pro
Entitlement IDpremium
Price$9.99 / month (single tier, no free trial)
RevenueCat projectCivic-af
Web APIhttps://api.civixapp.us

⚠️ These identifiers are hard-coded in the app (lib/purchases.ts). If you type civix_pro or premium even slightly differently in RevenueCat/ASC, purchases silently fail with "No offerings available." Match them exactly.


PART A — App Store Connect (do this first)

A1. Paid Applications Agreement ← without this, NOTHING sells

  • App Store Connect → Business (or Agreements, Tax, and Banking).
  • Sign the Paid Applications Agreement. Status must be Active.
  • Fill in Bank + Tax info (Apple won't activate paid apps without it).

A2. Create the subscription product

  • ASC → your app (Civix, App ID 6759538146) → Monetization → Subscriptions.
  • Create a Subscription Group (e.g. "Civix Premium"). One group is fine.
  • Inside it, (+) Create an auto-renewable subscription:
    • Reference Name: Civix Premium (internal only)
    • Product ID: civix_proMUST be exactly this
    • Duration: 1 Month
  • Subscription Price: add price → $9.99 / month (USD base; Apple auto-fills other countries).
  • Localization: add at least English — Display Name (e.g. "Civix Premium") + Description.
  • Review info: screenshot of the paywall + review notes.
  • Save. Status should become Ready to Submit (it submits with your app build).

A3. In-App Purchase Key (.p8) ← RevenueCat needs this (StoreKit 2)

  • ASC → Users and Access (top) → Integrations tab → left list: In-App Purchase.
  • (+) Generate In-App Purchase Key → name it RevenueCatGenerate.
  • Download the .p8 file — ⚠️ one-time download, save it safely.
  • Copy the Key ID (e.g. ABC123DEFG) and the Issuer ID (long 57246542-… at the top).
  • You'll upload all three (file + Key ID + Issuer ID) into RevenueCat in B1.

The "App-Specific Shared Secret (Legacy)" in RevenueCat → leave blank. The P8 key above replaces it. Don't bother generating it.

A4. App metadata / compliance (needed to submit, not for IAP wiring)

  • App Privacy labels: declare Microphone (AI voice interview) + any analytics/crash (Sentry).
  • Screenshots (all required device sizes), Age Rating, description with the "not affiliated with USCIS / U.S. Government" disclaimer, Privacy + Terms URLs.

PART B — RevenueCat (after ASC product + P8 exist)

B1. Create / finish the App (the screen you're on now)

  • Apps & providers → New App Store app:
    • App name: Civic-af (App Store)
    • App Bundle ID: com.civics.test
    • In-app purchase key: drag the .p8 from A3, paste Key ID + Issuer ID.
    • App-Specific Shared Secret (Legacy): leave blank.
    • Apple Small Business Program: check only if you enrolled (15% fee tier); otherwise unchecked.
    • Save.

B2. Products (Product catalog → Products)

  • (+) New / Import → add product civix_pro (import from the App Store app you just added; it should appear once ASC has it).
  • Confirm the product identifier reads exactly civix_pro.

B3. Entitlement (Product catalog → Entitlements)

  • (+) New entitlement → identifier premiumexactly this.
  • Attach product civix_pro to the premium entitlement. (This is what flips a buyer to "paid" in the app.)

B4. Offering (Product catalog → Offerings)

  • (+) New offering (e.g. identifier default).
  • Add a Package to it → attach product civix_pro. (Package id can be anything, e.g. $rc_monthly — the app finds it by the product id.)
  • Mark this offering as the "Current" / default. ← the app reads offerings.current; if nothing is current, the paywall shows no price.

B5. API keys (the two keys — different jobs, different homes)

KeyStarts withWhere in RevenueCatWhere it goes
Public Apple SDK keyappl_Apps → your iOS app → public SDK keythe app (EAS) → EXPO_PUBLIC_REVENUECAT_IOS_API_KEY
Secret keysk_API keys → Secret API keys → "CivixApp US" → Show keyVercelREVENUECAT_SECRET_API_KEY

🔒 The sk_ secret key goes only on the server (Vercel) — never in the app or GitHub. The appl_ public key goes only in the app — it's safe there. They are NOT interchangeable.


PART C — Wire the keys into the project

C1. App build (EAS) — the public key

  • Set in the production EAS environment (EAS dashboard → project → env, or eas secret):
    • EXPO_PUBLIC_REVENUECAT_IOS_API_KEY = appl_… (your public key from B5)
  • (Optional Android) EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEY = goog_…
  • Rebuild: eas build --platform ios --profile testflight (or production).

C2. Vercel (server) — the secret key

  • Vercel → project → Settings → Environment Variables (Production):
    • REVENUECAT_SECRET_API_KEY = sk_… (your secret key from B5)
  • Redeploy production (env changes don't apply to existing deployments).
  • Why it matters: entitlement.ts fails closed in production — without this key, real subscribers get 403'd out of the realtime voice interview. (TestFlight is unaffected.)

PART D — Test it works (Sandbox)

  • On a real device with a Sandbox Apple ID (ASC → Users and Access → Sandbox Testers).
  • Open the paywall → price shows $9.99/month (live from RevenueCat, not the fallback).
  • Tap subscribe → complete Sandbox purchase → premium unlocks immediately (locked features open, realtime voice no longer 403s).
  • Kill + reopen app → still premium.
  • Fresh install / re-login → Restore Purchases brings premium back.

Gotchas (the stuff that wastes hours)

  1. Ignore RevenueCat's "Paywalls" / Paywall Builder (the boxing template). Civix draws its own paywall in code; the hosted builder is never shown. Don't design or publish it.
  2. Identifiers are case-exact: civix_pro and premium. One typo → "No offerings available."
  3. Offering must be "Current" or the app sees no packages.
  4. Two keys, two homes: appl_ → app, sk_ → Vercel. Swapping them = broken.
  5. The price comes from ASC, not the code. Code's $9.99 is only a pre-load fallback; whatever you price civix_pro at in App Store Connect is what users actually see.
  6. Redeploy Vercel after setting the secret key, and rebuild the app after setting the public key — env changes need a new deploy/build to take effect.

TL;DR — the absolute minimum to go live

  1. ASC: sign Paid Apps agreement, create civix_pro ($9.99/mo) and civix_pro_annual ($59.99/yr), generate the .p8 key.
  2. RevenueCat: add app (com.civics.test + .p8), add both products, entitlement premium (attach both), one default Offering containing both packages (monthly + annual).
  3. Keys: appl_ → EAS app env (EXPO_PUBLIC_REVENUECAT_IOS_API_KEY, ✅ already in eas.json); sk_ → Vercel (REVENUECAT_SECRET_API_KEY) → redeploy.
  4. Sandbox-test purchase (both plans) + restore.

Pricing — two tiers (monthly + annual)

PlanProduct IDPriceNotes
Monthlycivix_pro$9.99/moAnchor
Annual ⭐civix_pro_annual$59.99/yr≈ $5/mo, "SAVE 50%" — the value driver, default-selected in the paywall
  • No free trial (decided): the existing free tier (64 questions, 1 AI interview/week, 1 mock/day) IS the "partial taste." Premium = full.
  • The code (paywall.tsx, purchases.ts) renders both tiers, defaults to annual, shows a per-day figure + "SAVE 50%" pill. Prices display live from the RevenueCat offering; the $9.99/$59.99 in code are only pre-load fallbacks — the real price is whatever you set in App Store Connect.
  • ⚠️ You must create both products in ASC and put both packages in the same RevenueCat offering, or the second plan won't appear.

Integration validated against official RevenueCat docs (react-native-purchases v9.6.9)

Cross-checked lib/purchases.ts against the RevenueCat React Native docs — the wiring is correct and complete:

  • Purchases.configure({ apiKey }) — correct (apiKey is the only required field).
  • Trusted Entitlements now enabled: entitlementVerificationMode: INFORMATIONAL — attaches a cryptographic verification result to customerInfo (tamper detection) without changing what's granted (zero lockout risk). Guarded for older SDKs.
  • getOfferings()current.availablePackages, find by product.identifier — correct.
  • purchasePackage(), restorePurchases(), getCustomerInfo(), entitlements.active['premium'], addCustomerInfoUpdateListener, getAppUserID() (cached for server verification) — all correct.

The core integration is 100% wired for launch on the custom paywall.


POST-LAUNCH: migrate to RevenueCat hosted Paywalls (the long-run home)

Why: remote paywall updates (no app release) + A/B testing / Experiments to optimize conversion. Do this week 1 after launch (it needs a native dependency = a new build, so not in the launch crunch).

Steps:

  1. Add the UI package (native dep → requires a new eas build, NOT OTA):
    npx expo install react-native-purchases-ui
    
  2. Design the paywall in the RevenueCat dashboard → Paywalls → attach it to the default Offering (the one with civix_pro + civix_pro_annual). (Ignore the boxing sample template — start from a blank/other template and brand it.)
  3. Present it in code (validated from the docs):
    import RevenueCatUI, { PAYWALL_RESULT } from 'react-native-purchases-ui';
    
    // Option A — present only if not already entitled:
    await RevenueCatUI.presentPaywallIfNeeded({ requiredEntitlementIdentifier: 'premium' });
    
    // Option B — always present, handle the result:
    const result = await RevenueCatUI.presentPaywall();
    // PAYWALL_RESULT.PURCHASED | RESTORED → unlocked; CANCELLED | ERROR | NOT_PRESENTED → not
    
  4. Swap the router.push('/paywall') calls to present the RevenueCat paywall instead.
  5. Nothing else changes — products, entitlement (premium), offering, purchases.ts, and server verification all carry over untouched. Only the screen swaps.
  6. Turn on Experiments in RevenueCat and start A/B testing price + copy.