Migrate Adapty Capacitor SDK to v4.1.1

Adapty Capacitor SDK 4.1.1 is the current stable release of the 4.x line — 4.0 was released only as a beta, so if you’re on 3.x, migrate straight to 4.1.1. This guide covers the whole migration: the flows introduced in 4.0 and the 4.1.1 changes on top of them.

The 4.x line introduces flows and renames the paywall APIs accordingly. The new APIs work with flows, and they still work with paywalls from the old builder — no setup changes are required on the Adapty Dashboard side. On top of that, 4.1.1 makes Adapty Attribution opt-in, renames the external attribution method, changes the fallback file format, and adds App Store promoted in-app purchases.

Note

Coming from the 4.0 beta? Replace the pinned beta version with the latest release, then only four sections apply: Adapty Attribution is disabled by default, the renamed external attribution APIs, fallback files, and App Store promoted in-app purchases.

Quick reference

v3v4.1.1
Adapty Attribution enabled automaticallydisabled by default — opt in with adaptyAttributionEnabled: true
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 + AdaptyFlowPaywall
createPaywallView(paywall, params?)createFlowView(flow, params?)
PaywallViewControllerFlowViewController
EventHandlers (type)FlowEventHandlers
CreatePaywallViewParamsInputCreateFlowViewParamsInput
onRenderingFailedonError
adapty.updateAttribution({ attribution, source })adapty.updateExternalAttribution({ attribution, provider })
AdaptyProfile.appliedAttributionSourcesAdaptyProfile.appliedExternalAttributionProviders
AttributionSourceAdaptyExternalAttributionProvider
Fallback file downloaded for 3.xnew fallback file format — download the file again
Promoted in-app purchases completed automatically, with no way to intercept themthe 'onPromotedPurchaseReceived' event and adapty.makePromotedPurchase({ product }) hand completion to your app

AdaptyPaywallProduct keeps its name — products still belong to a flow, and getPaywallProducts keeps its name too, now taking an AdaptyFlow. The getFlow and getFlowForDefaultAudience methods no longer take a locale parameter — pass it to createFlowView instead. The purchase and profile APIs (makePurchase, restorePurchases, getProfile, identify, updateProfile) and setFallback keep the same signatures, but the fallback file itself must be re-downloaded — see Fallback files. The view methods present, dismiss, setEventHandlers, clearEventHandlers, 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. The onboarding methods still work but are deprecated — see Onboarding API deprecation. Some default behaviors changed — see Default behavior changes.

Minimum versions

The runtime requirements are unchanged from v3.16+: iOS 15.0, Android minSdk 24, and Capacitor 8. No deployment-target changes are needed.

There is one new build requirement: Xcode 26 or later — the native Adapty iOS SDK bundled with this release uses Swift tools 6.2.

Installation

Update the package

npm install @adapty/capacitor@latest

Then sync the native projects:

npx cap sync

iOS: Swift Package Manager only

CocoaPods’ spec repo goes read-only in December 2026, so starting with v4 the AdaptyCapacitor.podspec is removed and the SDK installs on iOS only through Swift Package Manager (SPM). Your app’s iOS project must use Capacitor’s SPM integration:

  • New apps: add the iOS platform with the SPM package manager:
npx cap add ios --packagemanager SPM

See Install Adapty SDK for the full setup.

⚠️ Adapty Attribution is disabled by default

Warning

If you use Adapty Attribution and update to SDK 4.1.1 without opting in, it breaks silently — installs stop registering, and nothing warns you.

In earlier versions, the SDK registered installs for Adapty Attribution automatically. Starting from SDK version 4.1.1, this is off by default: the SDK doesn’t register installs, the 'onInstallationDetailsSuccess' and 'onInstallationDetailsFail' events never fire, and getCurrentInstallationStatus returns the not_available status.

If you use Adapty Attribution, enable it when activating the SDK:

  await adapty.activate({
    apiKey: 'YOUR_PUBLIC_SDK_KEY',
    params: {
+     adaptyAttributionEnabled: true,
    },
  });

If you don’t use Adapty Attribution, no changes are needed.

Fetching flows

getPaywall → getFlow

The returned type changes from AdaptyPaywall to AdaptyFlow, and the locale option moves from the fetch call to createFlowView; for custom paywalls, all locales are returned in flow.remoteConfigs:

- const paywall = await adapty.getPaywall({ placementId: 'YOUR_PLACEMENT_ID', locale: 'en' });
+ const flow = await adapty.getFlow({ placementId: 'YOUR_PLACEMENT_ID' });
+ const view = await createFlowView(flow, { locale: 'en' });

locale stays optional on createFlowView: omit it and the view renders in en, or in the flow’s default localization when the flow has no en. Because of that fallback, the view may render in a different localization than you asked for — the new FlowViewController.locale property reports which one it used. See Localizations and locale codes.

getPaywallForDefaultAudience is renamed the same way:

- const paywall = await adapty.getPaywallForDefaultAudience({ placementId: 'YOUR_PLACEMENT_ID', locale: 'en' });
+ const flow = await adapty.getFlowForDefaultAudience({ placementId: '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 });

Fallback files

The fallback file format changed in 4.0, and again in 4.1.1. Download the file again from Placements > Fallbacks and bundle it in your app, even if you already downloaded one for a 4.0 beta.

Warning

This step produces no build error. If you skip it, setFallback rejects the outdated file and every placement loses its fallback.

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').
productIdentifiersflow.paywalls[i].productIdentifiersProduct identifiers now live on each flow variation, not on the flow.
products (deprecated in v3)removedUse flow.paywalls[i].productIdentifiers, or call getPaywallProducts(flow) for full products. ProductReference is removed as a public type.
webPurchaseUrl?flow.paywalls[i].webPurchaseUrlMoved from the flow to each paywall variation.
version?: numberflowVersionId?: stringRenamed, and the type changed from number to string.
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.

requestLocale remains on AdaptyOnboarding — only the flow model drops it.

Product identifiers moved from the flow to each variation:

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

If your code still reads paywall.products — deprecated in v3 and now removed — switch to productIdentifiers, or call getPaywallProducts(flow) when you need full products rather than identifiers.

Web paywall methods

openWebPaywall and createWebPaywallUrl keep their names, but the paywallOrProduct option now takes an AdaptyFlowPaywall (a flow variation) instead of an AdaptyPaywall. You can still pass an AdaptyPaywallProduct. Check that flow.paywalls is non-empty before reading the first entry:

  const flow = await adapty.getFlow({ placementId: 'YOUR_PLACEMENT_ID' });
- await adapty.openWebPaywall({ paywallOrProduct: paywall });
+ await adapty.openWebPaywall({ paywallOrProduct: 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 Adapty — Adapty tracks those views automatically.

Displaying flows

createPaywallView → createFlowView

Rename the factory function and pass the AdaptyFlow. The returned controller is renamed from PaywallViewController to FlowViewController, but its methods (present, dismiss, setEventHandlers, clearEventHandlers, and showDialog) are unchanged. The params type is renamed from CreatePaywallViewParamsInput to CreateFlowViewParamsInput:

- import { createPaywallView } from '@adapty/capacitor';
+ import { createFlowView } from '@adapty/capacitor';

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

A flow view is single-use: after you call dismiss(), the view is destroyed and its event handlers are cleared, so call createFlowView again to present the flow once more.

New parameters

CreateFlowViewParamsInput keeps every v3 parameter (prefetchProducts, loadTimeoutMs, customTags, customTimers, customAssets, productPurchaseParams) and adds three:

Note

customTimers still exists, but it only affects legacy Paywall Builder paywalls. A flow’s countdown timer runs on the behavior set in the Flow & Paywall Builder, so a flow ignores whatever you pass here.

ParameterDescription
localeThe localization to render the flow with. It moved here from getPaywall — see getPaywall → getFlow.
customLayoutIdThe custom ID of a layout in the flow’s layout configuration. Pass it to render that specific layout instead of the one the SDK picks automatically from the device type and screen size. If no layout matches the ID, the call fails with a no-view-configuration error. The Flow & Paywall Builder doesn’t assign custom layout IDs yet, so leave this unset.
android.enableSafeAreaControls Android safe-area paddings at runtime. Nested under the android key, and defaults to true.
const view = await createFlowView(flow, {
  locale: 'en',
  customLayoutId: 'tablet_landscape',
  android: { enableSafeArea: true },
});

Handling events

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

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

All other event handlers keep their names. One changes its signature: onAppeared is now (view) instead of (), where view is a FlowEventView describing the view that appeared — including the localization it was built with. Existing handlers keep working, since they ignore the new argument. See Handle flow & paywall events for the full list.

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 Present flows in Observer mode.
  • onAnalytics: (name, params) — analytics events a flow emits, starting with a screen view for every screen the user opens. See Track flow screen views.
  • onRequestPermission: (permission, customArgs) — reserved for system-permission requests (such as push notifications or camera access) from a flow. Flows don’t trigger permission requests yet, so you don’t need to implement it.

Separately, 4.1.1 adds an SDK-level event rather than a flow handler: 'onPromotedPurchaseReceived', delivered through adapty.addListener. With no listener registered, the SDK completes the promoted purchase itself; registering one hands completion to your app. See App Store promoted in-app purchases.

Renamed external attribution APIs

Starting from SDK version 4.1.1, the APIs for passing attribution data from an external provider (Adjust, AppsFlyer, Branch, Tenjin, or a custom one) are renamed to match the native SDKs. There are no deprecated aliases, so existing call sites stop working until you rename them:

Before 4.1.14.1.1
adapty.updateAttribution({ attribution, source })adapty.updateExternalAttribution({ attribution, provider })
AttributionSourceAdaptyExternalAttributionProvider
AdaptyProfile.appliedAttributionSourcesAdaptyProfile.appliedExternalAttributionProviders

updateAttribution → updateExternalAttribution

The method is renamed and its source option becomes provider. Attribution data is still a plain object:

- await adapty.updateAttribution({ attribution, source: 'adjust' });
+ await adapty.updateExternalAttribution({ attribution, provider: 'adjust' });

AttributionSource → AdaptyExternalAttributionProvider

The provider type is renamed. It stays an open union — the predefined values are 'apple_search_ads', 'adjust', 'appsflyer', 'branch', and 'tenjin', and any other string is accepted, so a provider Adapty adds later works without an SDK update:

- import type { AttributionSource } from '@adapty/capacitor';
+ import type { AdaptyExternalAttributionProvider } from '@adapty/capacitor';

AdaptyProfile.appliedAttributionSources → appliedExternalAttributionProviders

The profile property that lists the attribution providers applied to the profile is renamed, and its element type changes accordingly:

- if (profile.appliedAttributionSources?.includes('apple_search_ads')) {
+ if (profile.appliedExternalAttributionProviders?.includes('apple_search_ads')) {
      // Apple Ads attribution has been applied
  }

Code that reads it needs updating — see Show an Apple Ads targeted paywall.

App Store promoted in-app purchases

Before 4.1.1, an in-app purchase promoted on your App Store product page completed on its own and Adapty recorded the transaction, but your app had no way to intercept it. 4.1.1 adds that hook, so this is a new capability rather than a migration step: without code of your own, the SDK still completes promoted purchases for you.

Write code only to take over completion yourself — for example to show a screen first. Register a listener for the new 'onPromotedPurchaseReceived' event, and complete the purchase with adapty.makePromotedPurchase. While that listener is registered, the SDK stops completing promoted purchases for you.

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.
  • Views are single-use: After dismiss(), the view is destroyed. Call createFlowView again to present the flow once more.

Removed APIs

Removed exports

These symbols are no longer exported from @adapty/capacitor. Remove their imports:

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

activate: lockMethodsUntilReady

lockMethodsUntilReady (already a deprecated no-op in v3) is removed. Remove it from your activate call — keeping it no longer compiles:

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

makePurchase: Android parameters

The deprecated flat Android shape of MakePurchaseParamsInput is removed — only the nested form remains. Move any Android purchase parameters into params: { android: { ... } }. See Make purchases for the full example.

Onboarding API deprecation

The legacy onboarding API is deprecated in v4 in favor of the Flow & Paywall Builder. It still works, but will be removed in a future release, so plan migration of your onboardings to the Flow & Paywall Builder.

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