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)
| Thing | Value |
|---|---|
| App name | Civix - US Citizenship Test |
| Bundle ID | com.civics.test |
| ASC App ID | 6759538146 |
| Apple ID (account) | mhashimi2019@icloud.com |
| Apple Team ID | 22GBQ3S2M7 |
| App version | 2.1.3 |
| Product ID (the subscription) | civix_pro |
| Entitlement ID | premium |
| Price | $9.99 / month (single tier, no free trial) |
| RevenueCat project | Civic-af |
| Web API | https://api.civixapp.us |
⚠️ These identifiers are hard-coded in the app (
lib/purchases.ts). If you typecivix_proorpremiumeven 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_pro← MUST be exactly this - Duration: 1 Month
- Reference Name:
- 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
RevenueCat→ Generate. - Download the
.p8file — ⚠️ one-time download, save it safely. - Copy the Key ID (e.g.
ABC123DEFG) and the Issuer ID (long57246542-…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
.p8from 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.
- App name:
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
premium← exactly this. - Attach product
civix_proto thepremiumentitlement. (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)
| Key | Starts with | Where in RevenueCat | Where it goes |
|---|---|---|---|
| Public Apple SDK key | appl_ | Apps → your iOS app → public SDK key | the app (EAS) → EXPO_PUBLIC_REVENUECAT_IOS_API_KEY |
| Secret key | sk_ | API keys → Secret API keys → "CivixApp US" → Show key | Vercel → REVENUECAT_SECRET_API_KEY |
🔒 The
sk_secret key goes only on the server (Vercel) — never in the app or GitHub. Theappl_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(orproduction).
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.tsfails 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)
- 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.
- Identifiers are case-exact:
civix_proandpremium. One typo → "No offerings available." - Offering must be "Current" or the app sees no packages.
- Two keys, two homes:
appl_→ app,sk_→ Vercel. Swapping them = broken. - The price comes from ASC, not the code. Code's
$9.99is only a pre-load fallback; whatever you pricecivix_proat in App Store Connect is what users actually see. - 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
- ASC: sign Paid Apps agreement, create
civix_pro($9.99/mo) andcivix_pro_annual($59.99/yr), generate the .p8 key. - RevenueCat: add app (
com.civics.test+ .p8), add both products, entitlementpremium(attach both), one default Offering containing both packages (monthly + annual). - Keys:
appl_→ EAS app env (EXPO_PUBLIC_REVENUECAT_IOS_API_KEY, ✅ already in eas.json);sk_→ Vercel (REVENUECAT_SECRET_API_KEY) → redeploy. - Sandbox-test purchase (both plans) + restore.
Pricing — two tiers (monthly + annual)
| Plan | Product ID | Price | Notes |
|---|---|---|---|
| Monthly | civix_pro | $9.99/mo | Anchor |
| 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.99in 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 tocustomerInfo(tamper detection) without changing what's granted (zero lockout risk). Guarded for older SDKs. - ✅
getOfferings()→current.availablePackages, find byproduct.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:
- Add the UI package (native dep → requires a new
eas build, NOT OTA):npx expo install react-native-purchases-ui - 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.) - 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 - Swap the
router.push('/paywall')calls to present the RevenueCat paywall instead. - Nothing else changes — products, entitlement (
premium), offering,purchases.ts, and server verification all carry over untouched. Only the screen swaps. - Turn on Experiments in RevenueCat and start A/B testing price + copy.