Adapty
Set up Adapty for payments in an Expo app with ReadyNative: in-app subscriptions + Paywall Builder · dev build required (no Expo Go)
Expo Go: no - dev build required
Ships native code. Make a dev build with bunx expo run:ios / bunx expo run:android (or bunx eas-cli build --profile development).
This gives you in-app subscriptions through Adapty: src/lib/payments.ts implements the shim on react-native-adapty (its active list holds access-level ids such as premium), a PaymentsProvider (order 70) activates the SDK once and listens on onLatestProfileLoad, and /paywall ships as a modal with your feature bullets, the placement's products with localized price and period, Buy, Restore, and Terms/Privacy links from readynative.config.ts urls. If you set up with --with-examples there's an example at /examples/paywall and a jest mock for the SDK. @/lib/payments keeps the same API whichever option you pick - pick Adapty when you want its no-code Paywall Builder and A/B tests on placements.
Setup
bun run setup --payments adapty- Create an app at app.adapty.io and add the iOS bundle id (from
readynative.config.tsapp.ios.bundleId, plus.devfor the dev variant) and the Android package under [App settings]. - Open [App settings] → [General] and copy the Public SDK key (
public_live_…) into.envasEXPO_PUBLIC_ADAPTY_PUBLIC_KEY. It's public and serves both platforms. Without it the module no-ops:useEntitlements()returns{ active: [], loading: false }and/paywallshows "Configure Adapty". - Create the subscriptions in [App Store Connect] → [Subscriptions] and [Play Console] → [Monetize] → [Subscriptions], then add them in Adapty under [Products].
- Open [Access levels] and use
premium(Adapty's built-in default, and what the examples check for), then attach your products to it. - Open [Paywalls], build one with the Paywall Builder, and attach your monthly product to it.
- Open [Placements] and attach that paywall to a placement. The default placement id is
default; if you use another one, put it in.envasEXPO_PUBLIC_ADAPTY_PLACEMENT_ID. - Add a sandbox tester in [App Store Connect] → [Users and Access] → [Sandbox] → [Testers], and a license tester in [Play Console] → [Setup] → [License testing]. Purchases only work on a real device signed into one of those accounts.
- Make a dev build -
npx expo run:ios,npx expo run:android, oreas build --profile development. The module also patches the app config with["expo-build-properties", { ios: { useFrameworks: "dynamic" } }], which the SDK's Swift Package dependencies require.
Going to production?
Make sure every product id exists and is approved in App Store Connect and Play Console - a product that only lives in Adapty never shows a price. Check that the paywall you want live is the one attached to the placement EXPO_PUBLIC_ADAPTY_PLACEMENT_ID reads, because that's the only one the app asks for.
- Run
bun run doctor- every row for this module should be green.
The keys, in one place:
| Key | Where to get it |
|---|---|
EXPO_PUBLIC_ADAPTY_PUBLIC_KEY | Adapty → App settings → General → Public SDK key (public_live_…) |
EXPO_PUBLIC_ADAPTY_PLACEMENT_ID | Adapty → Placements → the placement your paywall is attached to (default default) |
Dashboards: App settings · Products · Paywalls · Placements · App Store Connect sandbox testers · Play Console license testers. Deps: react-native-adapty ^4.1.0 (RN ≥ 0.75, one package for the SDK and the Paywall Builder UI) and expo-build-properties.
Usage
Gate a feature on an access level:
import { payments } from "@/lib/payments";
const { active, loading } = payments.useEntitlements(); // active: ["premium"]
if (!loading && !active.includes("premium")) await payments.presentPaywall?.();Sell from your own screen:
import { getPaywallFlow, getPaywallProducts, purchaseProduct } from "@/lib/payments";
const flow = await getPaywallFlow();
const products = await getPaywallProducts(flow);
const bought = await purchaseProduct(products[0]); // false when the user cancels or the purchase is pendingRestore, which the stores require you to offer:
import { payments } from "@/lib/payments";
await payments.restorePurchases();presentPaywall() shows the no-code paywall when the flow has a view configuration; a remote-config-only paywall opens /paywall, which lists the placement's products instead.
Gotchas
- Expo Go can't run this module - native StoreKit and Play Billing need a dev build. In Expo Go the SDK falls back to its mock mode: no real products, no purchases.
expo-build-propertiesios.useFrameworks: "dynamic"applies to the whole iOS project. If another native library refuses dynamic frameworks, follow its docs (usually ause_frameworks! :linkage => :dynamicexception) - Adapty SDK 4.x needs the setting.react-native-adaptyhas no JS implementation of the bridge under jest. The root__mocks__file is picked up automatically - don't addjest.mock()for it.adapty.activate()throws when called twice, so the provider passes__ignoreActivationOnFastRefreshin dev.adapty.activate()also passesipAddressCollectionDisabled: !features.tracking,ios: { idfaCollectionDisabled: !features.tracking }andandroid: { adIdCollectionDisabled: !features.tracking }, so Adapty collects no IDFA, GAID or IP address unlessfeatures.trackingis on inreadynative.config.ts.presentPaywall()resolves once the view is presented, not when it closes; entitlements arrive throughonLatestProfileLoador the view'sonPurchaseCompletedhandler.
Check it works (the Examples steps need a tree set up with --with-examples):
- Set up the dashboard as above: app, bundle id and package, products, the
premiumaccess level, and a paywall with a monthly product attached to the placement. - Put
EXPO_PUBLIC_ADAPTY_PUBLIC_KEYin.envand runnpx expo run:ioson a real device signed into a sandbox Apple ID (Settings → App Store → Sandbox Account). - Open Examples → "Paywall & entitlements" and expect "none active".
- Tap "Open paywall": the Paywall Builder paywall appears (or
/paywallwhen the paywall has no builder view) with the product and a localized price. Buy → sandbox sheet → confirm → the sheet closes with a toast "Purchase complete" and access levels showpremium. - Delete and reinstall the app, then tap "Restore purchases": access levels show
premiumagain. - Repeat on Android with a license-tester Google account.
Remove it
While modules/ exists (a tree set up with --keep-modules), setup does all of it:
bun run setup --payments none --yes --keep-modulesIn a finalized tree setup is a stub, so you undo it by hand. Here is everything this module added:
- Delete the files that are still there (the demo screens are gone already unless you set up with
--with-examples):__mocks__/react-native-adapty.ts,src/app/examples/paywall.tsx,src/app/paywall/_layout.tsx,src/app/paywall/index.tsx,src/lib/__tests__/payments-adapty.test.ts,src/screens/examples/paywall-example-screen.tsx,src/screens/paywall/paywall-screen.tsx. - Replace, don't delete
src/lib/payments.ts: core code imports it, so swap in the no-op version frommodules/payments/none/files/of a fresh clone of your tier repo - same exports, nothing behind them. - Uninstall the dependencies:
bun remove expo-build-properties react-native-adapty. - Drop the config plugin
expo-build-propertiesfrom.readynative.json→modules.app.expo.plugins(that is whereapp.config.tsreads it from), then rebuild the dev build. - Unwrap the provider: delete
<PaymentsProvider>and its import fromsrc/providers.tsx. - Remove the env keys
EXPO_PUBLIC_ADAPTY_PUBLIC_KEY,EXPO_PUBLIC_ADAPTY_PLACEMENT_IDfrom.env,.env.exampleand your EAS environment, andEXPO_PUBLIC_ADAPTY_PUBLIC_KEY,EXPO_PUBLIC_ADAPTY_PLACEMENT_IDfromsrc/lib/env.ts. - Update the privacy declarations: remove this module's entries from
.readynative.json→modules.app.expo.ios.privacyManifests, then re-runbun run gen:privacyand revise your store privacy answers. - Check it:
bun run typecheckandbun run lintpoint at anything that still imports the removed files;bun run gen:graphrefreshesdocs/ARCHITECTURE.md.
Reference
Everything below is generated from modules/payments/adapty/module.json - the same file bun run setup reads, so it is what actually lands in your repo.
Install
bun run setup --payments adaptyModule id: payments/adapty.
Dependencies
| Package | Version | Kind |
|---|---|---|
expo-build-properties | ~57.0.17 | dependency |
react-native-adapty | ^4.1.0 | dependency |
Config plugins
Merged into app.config.ts through .readynative.json (modules.app):
expo-build-properties(with options)
Environment keys
| Key | Required | Server-only | Example | Docs |
|---|---|---|---|---|
EXPO_PUBLIC_ADAPTY_PUBLIC_KEY | yes | no | public_live_xxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxx | dashboard |
EXPO_PUBLIC_ADAPTY_PLACEMENT_ID | no | no | default | dashboard |
Keys go in .env (see .env.example). Required keys are checked by bun run doctor; Server-only keys have no EXPO_PUBLIC_ prefix, are read only by API routes and never reach the bundle.
Privacy
Play Data safety draft: collects Purchase history, Profile id (anonymous by default), Device info; shared with Adapty (processor). Source of truth: vendor disclosure.
Apple privacy manifest data types (composed into ios.privacyManifests by setup):
| Type | Linked to user | Tracking | Purposes |
|---|---|---|---|
PurchaseHistory | yes | no | AppFunctionality |
UserID | yes | no | AppFunctionality |
Providers
Rendered in src/providers.tsx (lower order = outermost):
| Order | Provider | From |
|---|---|---|
| 70 | PaymentsProvider | @/lib/payments |
Doctor checks
- env:
EXPO_PUBLIC_ADAPTY_PUBLIC_KEY,EXPO_PUBLIC_ADAPTY_PLACEMENT_ID - dev build toolchain (Xcode / Android SDK)
After setup
- Adapty: create an app at https://app.adapty.io, add the App Store bundle id and Play package name from readynative.config.ts (App settings → iOS SDK / Android SDK).
- Adapty: copy the Public SDK key (App settings → General) into .env as EXPO_PUBLIC_ADAPTY_PUBLIC_KEY (one key serves both platforms).
- Adapty: create Products (App Store Connect / Play Console product ids), keep the default access level
premium(or rename it in src/lib/payments.ts), build a Paywall and attach it to a Placement; put the placement id into .env as EXPO_PUBLIC_ADAPTY_PLACEMENT_ID (defaults todefault). - react-native-adapty is a native module and needs
expo-build-propertieswithios.useFrameworks: "dynamic"(added to app config by this module): make a dev build withnpx expo run:ios/npx expo run:android(oreas build --profile development). Expo Go only runs the SDK's mock mode.
Files
8 files copied to the project root
__mocks__/react-native-adapty.tssrc/app/examples/paywall.tsxsrc/app/paywall/_layout.tsxsrc/app/paywall/index.tsxsrc/lib/__tests__/payments-adapty.test.tssrc/lib/payments.tssrc/screens/examples/paywall-example-screen.tsxsrc/screens/paywall/paywall-screen.tsx