Migrate Adapty React Native SDK to v. 4.0

Adapty React Native SDK 4.0 (beta) introduces flows and renames the paywall APIs accordingly. The new APIs work with both the new Flow Builder and the existing Paywall Builder — no setup changes are required on the Adapty Dashboard side.

Quick reference

v3v4
adapty.getPaywall(placementId, locale?, params?)adapty.getFlow(placementId, params?)
adapty.getPaywallForDefaultAudience(placementId, locale?, params?)adapty.getFlowForDefaultAudience(placementId, params?)
adapty.getPaywallProducts(paywall)adapty.getPaywallProducts(flow)
adapty.logShowPaywall(paywall)adapty.logShowFlow(flow)
AdaptyPaywall (type)AdaptyFlow
createPaywallView(paywall)createFlowView(flow)
AdaptyPaywallView (component)AdaptyFlowView
EventHandlers (type)FlowEventHandlers
onPaywallShownonAppeared
onPaywallClosedonDisappeared
onRenderingFailedonError

AdaptyPaywallProduct keeps its name — products still belong to a flow, and getPaywallProducts now takes an AdaptyFlow. The getFlow and getFlowForDefaultAudience methods no longer take a locale parameter. The view methods present, dismiss, setEventHandlers, and showDialog, and the event handlers onCloseButtonPress, onUrlPress, onCustomAction, onProductSelected, onPurchaseStarted, onPurchaseCompleted, onPurchaseFailed, onRestoreStarted, onRestoreCompleted, onRestoreFailed, onLoadingProductsFailed, onWebPaymentNavigationFinished, and onAndroidSystemBack keep the same names as in v3. Some default behaviors changed — see Default behavior changes.

Minimum iOS version

Adapty React Native SDK 4.0 raises the minimum iOS deployment target from iOS 13.0 to iOS 15.0. Set your iOS deployment target to 15.0 or later before upgrading.

Installation

Update the package

v4.0 is a pre-release, so pin the exact version — npm does not select pre-release versions through caret/tilde ranges:

npm install [email protected]
# or
yarn add [email protected]

iOS: native SDKs now come through Swift Package Manager

CocoaPods’ spec repo goes read-only in December 2026, so starting with v4 the native Adapty, AdaptyUI, and AdaptyPlugin SDKs are no longer pulled as CocoaPods sub-dependencies — the podspec pulls them through Swift Package Manager (via the spm_dependency helper). Two things this requires:

  • React Native 0.75 or later — needed for the spm_dependency podspec helper. On an older version, pod install fails with an explicit error; upgrade React Native first, or stay on react-native-adapty 3.x.
  • Dynamic frameworks — SPM dependencies require dynamic linkage. The way you enable this differs for Expo and bare React Native.

Expo

Add the expo-build-properties config plugin and set the iOS frameworks to dynamic in app.json (or app.config.js):

{
  "expo": {
    "plugins": [
      [
        "expo-build-properties",
        {
          "ios": {
            "useFrameworks": "dynamic"
          }
        }
      ]
    ]
  }
}

Then install the plugin and regenerate the native project:

npx expo install expo-build-properties
npx expo prebuild --clean

Bare React Native

Add dynamic frameworks to your iOS target, then reinstall pods:

use_frameworks! :linkage => :dynamic
cd ios && pod install --repo-update

If you previously pulled Adapty, AdaptyUI, or AdaptyPlugin as CocoaPods sub-dependencies, remove any explicit pod 'Adapty', pod 'AdaptyUI', or pod 'AdaptyPlugin' lines from your Podfile first.

Switching from the default static linkage to dynamic frameworks can conflict with libraries that don’t yet support modular headers, and is incompatible with Flipper. If you hit build issues, see this write-up on integrating Swift Package Manager with React Native libraries.

See Install Adapty SDK for the full setup.

Fetching flows

getPaywall → getFlow

The returned type changes from AdaptyPaywall to AdaptyFlow, and the locale parameter is removed — when you render a flow, the locale is resolved automatically; for custom paywalls, all locales are returned in flow.remoteConfigs:

- const paywall = await adapty.getPaywall('YOUR_PLACEMENT_ID', 'en');
+ const flow = await adapty.getFlow('YOUR_PLACEMENT_ID');

getPaywallForDefaultAudience is renamed the same way:

- const paywall = await adapty.getPaywallForDefaultAudience('YOUR_PLACEMENT_ID', 'en');
+ const flow = await adapty.getFlowForDefaultAudience('YOUR_PLACEMENT_ID');

getPaywallProducts(paywall) → getPaywallProducts(flow)

getPaywallProducts keeps its name but now takes an AdaptyFlow:

- const products = await adapty.getPaywallProducts(paywall);
+ const products = await adapty.getPaywallProducts(flow);

Data model

getFlow returns an AdaptyFlow instead of an AdaptyPaywall, and the object shape changed:

v3 AdaptyPaywall fieldv4 AdaptyFlow fieldAction
remoteConfig? (single)remoteConfigs?: AdaptyRemoteConfig[] (array)A flow carries one remote config per configured language. Read the one that matches the user: flow.remoteConfigs?.find((c) => c.lang === 'en').
productsflow.paywalls[i].productIdentifiersProduct identifiers now live on each flow variation, not on the flow.
webPurchaseUrl?flow.paywalls[i].webPurchaseUrlMoved from the flow to each paywall variation.
version?: numberflowVersionId?: stringRenamed, and the type changed from number to string.
hasViewConfigurationremovedRemove any hasViewConfiguration check from your code.
requestLocaleremovedThe locale is no longer part of the model.
(new)paywalls: AdaptyFlowPaywall[]Each entry is one paywall variation in the flow.
(new)responseCreatedAt: numberServer response timestamp, in milliseconds.

Product identifiers moved from the flow to each variation:

- const ids = paywall.products;
+ const ids = flow.paywalls[0].productIdentifiers;

Web paywall methods

openWebPaywall and createWebPaywallUrl keep their names, but the first argument is now an AdaptyFlowPaywall (a flow variation) instead of an AdaptyPaywall. You can still pass an AdaptyPaywallProduct.

  const flow = await adapty.getFlow('YOUR_PLACEMENT_ID');
- await adapty.openWebPaywall(paywall);
+ await adapty.openWebPaywall(flow.paywalls[0]);

Tracking flow views

logShowPaywall → logShowFlow

logShowPaywall is renamed to logShowFlow and now takes an AdaptyFlow. The event is still logged against the same variation, so existing funnel and A/B test metrics continue to work without dashboard changes.

- await adapty.logShowPaywall(paywall);
+ await adapty.logShowFlow(flow);

As in v3, you do not need to call this method when displaying flows or paywalls rendered by the Flow Builder or the Paywall Builder — Adapty tracks those views automatically.

Displaying flows

createPaywallView → createFlowView

Rename the factory function and pass the AdaptyFlow. The returned controller’s methods (present, dismiss, setEventHandlers, showDialog) are unchanged:

- import { createPaywallView } from 'react-native-adapty';
+ import { createFlowView } from 'react-native-adapty';

- const view = await createPaywallView(paywall);
+ const view = await createFlowView(flow);
  await view.present();

AdaptyPaywallView → AdaptyFlowView

If you render with the React component, rename it and pass the flow prop:

- import { AdaptyPaywallView } from 'react-native-adapty';
+ import { AdaptyFlowView } from 'react-native-adapty';

- <AdaptyPaywallView paywall={paywall} /* … */ />
+ <AdaptyFlowView flow={flow} /* … */ />

A flow view created with createFlowView is single-use: after you call dismiss(), the view is destroyed, so call createFlowView again to present the flow once more. An embedded AdaptyFlowView is dismissed by unmounting it — returning true from a handler does not close an embedded view, so change your own state instead, for example in onCloseButtonPress.

Handling events

The event-handler interface is renamed from EventHandlers to FlowEventHandlers, and three callbacks are renamed. Existing handler bodies don’t need code changes — just rename:

- onPaywallShown: () => { /* … */ },
+ onAppeared: () => { /* … */ },

- onPaywallClosed: () => { /* … */ },
+ onDisappeared: () => { /* … */ },

- onRenderingFailed: (error) => { /* … */ },
+ onError: (error) => { /* … */ },

All other event handlers keep their names. Two also gain a second argument: onPurchaseCompleted is now (purchaseResult, product) and onPurchaseFailed is now (error, product), where product is the AdaptyPaywallProduct involved. See Handle flow & paywall events for the full list.

onDisappeared fires only for a flow presented modally with createFlowView().present(). The AdaptyFlowView component does not expose it as a prop — dismiss an embedded view by unmounting it.

v4 also adds a few capabilities you can opt into:

  • adapty.openWebUrl(url, openIn?) and adapty.requestAppReview() methods — these back the default onUrlPress and onRequestAppReview handlers, so URLs and app-review prompts are handled natively out of the box. Call them directly only if you override those handlers.
  • Observer-mode purchase handling inside flows via the new onObserverPurchaseInitiated / onObserverRestoreInitiated handlers. See Handle purchases in observer mode.

Removed and deprecated APIs

setFallbackPaywalls → setFallback

setFallbackPaywalls is removed. Use setFallback, which takes the same argument:

- await adapty.setFallbackPaywalls(fileLocation);
+ await adapty.setFallback(fileLocation);

Removed exports

These symbols are no longer exported from react-native-adapty. Remove their imports:

  • AdaptyPaywall: Use AdaptyFlow instead.
  • ProductReference: Use AdaptyProductIdentifier, read from flow.paywalls[i].productIdentifiers.
  • AdaptyPaywallBuilder: Removed. Flows and paywalls render natively.
  • AdaptyAndroidSubscriptionUpdateParameters: Use the nested subscriptionUpdateParams shape (see below).

activate: lockMethodsUntilReady

lockMethodsUntilReady is removed and this behavior is now always on. Remove it from your activate call — keeping it no longer compiles:

- await adapty.activate('PUBLIC_SDK_KEY', { lockMethodsUntilReady: true });
+ await adapty.activate('PUBLIC_SDK_KEY');

makePurchase: Android subscription update

The flat Android subscription-update shape is removed. Move oldSubVendorProductId and prorationMode into a nested subscriptionUpdateParams object, and keep isOfferPersonalized at the top level. See Make purchases for the full example.

Android: safe-area paddings

The Android boolean resource <bool name="adapty_paywall_enable_safe_area_paddings">…</bool> is removed. Delete it from res/values/bools.xml and control safe-area paddings at runtime with the enableSafeArea parameter when you create the flow view. It defaults to true for modal presentation and false for the embedded component.

Mock mode

If you run the SDK in mock mode (Expo Go or web preview), rename the mock config key paywalls to flows.

Default behavior changes

These changes do not cause compile errors, so test them at runtime:

  • onAndroidSystemBack: The default changed from closing the view to keeping it open. To restore the previous behavior, return true from the handler.
  • onPurchaseCompleted: The default changed from closing the view (unless the user canceled the purchase) to always keeping it open. To restore the previous behavior, return purchaseResult.type !== 'user_cancelled' from the handler.
  • onRestoreCompleted: The default changed from closing the view after a successful restore to keeping it open. To restore the previous behavior, return true from the handler.
  • onUrlPress: The default now opens the URL through the native layer, honoring the in-app or external browser setting from the dashboard. Override the handler to open URLs yourself.

Onboarding API deprecation

The legacy onboarding API is deprecated in v4.0 in favor of the Flow Builder. It still works, and your IDE flags the deprecated symbols through their @deprecated annotations — there are no runtime warnings. These symbols will be removed in a future release, so plan migration of your onboardings to the Flow Builder.

Deprecated symbols: getOnboarding, getOnboardingForDefaultAudience, createOnboardingView, and AdaptyOnboardingView.