Migrate Adapty iOS SDK to v. 4.0

Adapty iOS 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:)Adapty.getFlow(placementId:)
AdaptyUI.getPaywallConfiguration(forPaywall:)AdaptyUI.getFlowConfiguration(forFlow:locale:)
Adapty.getPaywallProducts(paywall:)Adapty.getPaywallProducts(flow:)
Adapty.logShowPaywall(_:)Adapty.logShowFlow(_:)
AdaptyPaywallControllerAdaptyFlowController
AdaptyPaywallControllerDelegateAdaptyFlowControllerDelegate
AdaptyUI.paywallController(with:delegate:)AdaptyUI.flowController(with:delegate:)
.paywall() (SwiftUI modifier).flow()
AdaptyPaywallViewAdaptyFlowView
didFailRenderingWith: / didFailRendering:didReceiveError:
Adapty.updateAttribution(_:source:) (source: String)Adapty.updateAttribution(_:source:) (source: AdaptyAttributionSource)
Adapty.setIntegrationIdentifier(key:value:)Adapty.setIntegrationIdentifier(_:) (AdaptyIntegrationIdentifier)

Installation: CocoaPods no longer supported

Adapty iOS SDK 4.0 drops CocoaPods support. Install the SDK with Swift Package Manager.

If your project still uses CocoaPods, remove the Adapty and AdaptyUI pods from your Podfile, run pod install to clean them up, then add the package in Xcode via File → Add Package Dependency using https://github.com/adaptyteam/AdaptySDK-iOS.git.

Removed APIs

  • Adapty.getPaywallProductsWithoutDeterminingOffer(paywall:) — removed. All products now include offer information, so the separate eligibility pass is no longer needed.
  • AdaptyPaywallProductWithoutDeterminingOffer — removed. Callbacks that previously passed this type (such as didSelectProduct) now pass AdaptyPaywallProduct.

Fetching paywalls

getPaywall + getPaywallConfiguration → getFlow + getFlowConfiguration

The returned types change from AdaptyPaywall / AdaptyUI.PaywallConfiguration to AdaptyFlow / AdaptyUI.FlowConfiguration. The locale parameter moves out of the fetch call and onto getFlowConfiguration:

- let paywall = try await Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en")
- let paywallConfiguration = try await AdaptyUI.getPaywallConfiguration(forPaywall: paywall)
+ let flow = try await Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID")
+ let flowConfiguration = try await AdaptyUI.getFlowConfiguration(forFlow: flow, locale: "en")

getPaywallProducts(paywall:) → getPaywallProducts(flow:)

getPaywallProducts now takes an AdaptyFlow returned by Adapty.getFlow:

- let products = try await Adapty.getPaywallProducts(paywall: paywall)
+ let products = try await Adapty.getPaywallProducts(flow: flow)

Tracking paywall views

logShowPaywall(:) → logShowFlow(:)

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

- try await Adapty.logShowPaywall(paywall)
+ try 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.

UIKit

AdaptyPaywallController → AdaptyFlowController

Rename the controller type and the factory method:

- let controller = try AdaptyUI.paywallController(
-     with: paywallConfiguration,
-     delegate: self
- )
+ let controller = try AdaptyUI.flowController(
+     with: flowConfiguration,
+     delegate: self
+ )

AdaptyPaywallControllerDelegate → AdaptyFlowControllerDelegate

Rename the protocol and update every method signature. Note that didSelectProduct now receives AdaptyPaywallProduct instead of the removed AdaptyPaywallProductWithoutDeterminingOffer.

- class YourClass: AdaptyPaywallControllerDelegate {
+ class YourClass: AdaptyFlowControllerDelegate {

-     func paywallControllerDidAppear(_ controller: AdaptyPaywallController) { }
+     func flowControllerDidAppear(_ controller: AdaptyFlowController) { }

-     func paywallControllerDidDisappear(_ controller: AdaptyPaywallController) { }
+     func flowControllerDidDisappear(_ controller: AdaptyFlowController) { }

-     func paywallController(_ controller: AdaptyPaywallController,
-                            didPerform action: AdaptyUI.Action) { }
+     func flowController(_ controller: AdaptyFlowController,
+                         didPerform action: AdaptyUI.Action) { }

-     func paywallController(_ controller: AdaptyPaywallController,
-                            didSelectProduct product: AdaptyPaywallProductWithoutDeterminingOffer) { }
+     func flowController(_ controller: AdaptyFlowController,
+                         didSelectProduct product: AdaptyPaywallProduct) { }

-     func paywallController(_ controller: AdaptyPaywallController,
-                            didStartPurchase product: AdaptyPaywallProduct) { }
+     func flowController(_ controller: AdaptyFlowController,
+                         didStartPurchase product: AdaptyPaywallProduct) { }

-     func paywallController(_ controller: AdaptyPaywallController,
-                            didFinishPurchase product: AdaptyPaywallProduct,
-                            purchaseResult: AdaptyPurchaseResult) { }
+     func flowController(_ controller: AdaptyFlowController,
+                         didFinishPurchase product: AdaptyPaywallProduct,
+                         purchaseResult: AdaptyPurchaseResult) { }

-     func paywallController(_ controller: AdaptyPaywallController,
-                            didFailPurchase product: AdaptyPaywallProduct,
-                            error: AdaptyError) { }
+     func flowController(_ controller: AdaptyFlowController,
+                         didFailPurchase product: AdaptyPaywallProduct,
+                         error: AdaptyError) { }

-     func paywallControllerDidStartRestore(_ controller: AdaptyPaywallController) { }
+     func flowControllerDidStartRestore(_ controller: AdaptyFlowController) { }

-     func paywallController(_ controller: AdaptyPaywallController,
-                            didFinishRestoreWith profile: AdaptyProfile) { }
+     func flowController(_ controller: AdaptyFlowController,
+                         didFinishRestoreWith profile: AdaptyProfile) { }

-     func paywallController(_ controller: AdaptyPaywallController,
-                            didFailRestoreWith error: AdaptyError) { }
+     func flowController(_ controller: AdaptyFlowController,
+                         didFailRestoreWith error: AdaptyError) { }

-     func paywallController(_ controller: AdaptyPaywallController,
-                            didFailRenderingWith error: AdaptyUIError) { }
+     func flowController(_ controller: AdaptyFlowController,
+                         didReceiveError error: AdaptyUIError) { }

-     func paywallController(_ controller: AdaptyPaywallController,
-                            didFailLoadingProductsWith error: AdaptyError) -> Bool { }
+     func flowController(_ controller: AdaptyFlowController,
+                         didFailLoadingProductsWith error: AdaptyError) -> Bool { }

-     func paywallController(_ controller: AdaptyPaywallController,
-                            didPartiallyLoadProducts failedIds: [String]) { }
+     func flowController(_ controller: AdaptyFlowController,
+                         didPartiallyLoadProducts failedIds: [String]) { }

-     func paywallController(_ controller: AdaptyPaywallController,
-                            didFinishWebPaymentNavigation product: AdaptyPaywallProduct?,
-                            error: AdaptyError?) { }
+     func flowController(_ controller: AdaptyFlowController,
+                         didFinishWebPaymentNavigation product: AdaptyPaywallProduct?,
+                         error: AdaptyError?) { }
 }

SwiftUI

.paywall() modifier → .flow()

Rename the modifier and update the configuration parameter name:

 @State var flowPresented = false // rename freely — the variable name is your choice

 var body: some View {
     Text("Hello, AdaptyUI!")
-        .paywall(
+        .flow(
             isPresented: $flowPresented,
-            paywallConfiguration: paywallConfiguration,
+            flowConfiguration: flowConfiguration,
             didFailPurchase: { product, error in /* handle the error */ },
             didFinishRestore: { profile in /* check access level and dismiss */ },
             didFailRestore: { error in /* handle the error */ },
-            didFailRendering: { error in flowPresented = false }
+            didReceiveError: { error in flowPresented = false }
         )
 }

The renamed callback fires for the same rendering errors as didFailRendering did, plus new runtime errors from the flow script (JavaScript exceptions on AdaptyUIError code 4105.jsException). Existing handler bodies do not need code changes — just rename the parameter.

AdaptyPaywallView → AdaptyFlowView

Rename the view, update the configuration parameter, and update any didSelectProduct closure — it now receives AdaptyPaywallProduct instead of the removed AdaptyPaywallProductWithoutDeterminingOffer:

- AdaptyPaywallView(
-     paywallConfiguration: paywallConfiguration,
-     didSelectProduct: { product: AdaptyPaywallProductWithoutDeterminingOffer in /* handle */ },
+ AdaptyFlowView(
+     flowConfiguration: flowConfiguration,
+     didSelectProduct: { product: AdaptyPaywallProduct in /* handle */ },
     didFailPurchase: { product, error in /* handle the error */ },
     didFinishRestore: { profile in /* check access level and dismiss */ },
     didFailRestore: { error in /* handle the error */ },
-    didFailRendering: { error in /* handle the error */ }
+    didReceiveError: { error in /* handle the error */ }
 )

AdaptyUI custom assets

AdaptyUICustomVideoAsset

Two changes affect every existing call site:

  • .player now takes AVPlayer instead of AVQueuePlayer.
  • Every case gained a trailing resolution: CGSize? parameter. Pass nil to keep the current behavior, or pass the actual pixel size so the player can reserve layout space (aspect ratio = width / height) before the video loads.
- case file(url: URL, preview: AdaptyUICustomImageAsset?)
- case remote(url: URL, preview: AdaptyUICustomImageAsset?)
- case player(item: AVPlayerItem, player: AVQueuePlayer, preview: AdaptyUICustomImageAsset?)
+ case file(url: URL, preview: AdaptyUICustomImageAsset?, resolution: CGSize?)
+ case remote(url: URL, preview: AdaptyUICustomImageAsset?, resolution: CGSize?)
+ case player(item: AVPlayerItem, player: AVPlayer, preview: AdaptyUICustomImageAsset?, resolution: CGSize?)

Attribution and integration identifiers

updateAttribution(_:source:)

The source parameter changes from String to the new AdaptyAttributionSource type, and the previously nested AdaptyProfile.AttributionSource is renamed to the top-level AdaptyAttributionSource. Use one of the predefined sources, or pass a string literal for any other source — AdaptyAttributionSource conforms to ExpressibleByStringLiteral, so existing string-literal calls keep compiling.

- try await Adapty.updateAttribution(attribution, source: "adjust")
+ try await Adapty.updateAttribution(attribution, source: .adjust)

Predefined sources: .appleAds, .adjust, .appsflyer, .branch, .tenjin. If you keep the source in a String variable, wrap it: AdaptyAttributionSource(rawValue: yourSource).

setIntegrationIdentifier(_:)

setIntegrationIdentifier(key:value:) is replaced by a variadic method that takes one or more AdaptyIntegrationIdentifier values. Use the predefined factory methods instead of raw string keys:

- try await Adapty.setIntegrationIdentifier(key: "appsflyer_id", value: uid)
+ try await Adapty.setIntegrationIdentifier(.appsflyerId(uid))

You can set several identifiers in a single call:

try await Adapty.setIntegrationIdentifier(
    .appsflyerId(uid),
    .adjustDeviceId(adid)
)

Replace each old key string with its factory method:

v3 keyv4 factory
"adjust_device_id".adjustDeviceId(_:)
"airbridge_device_id".airbridgeDeviceId(_:)
"amplitude_user_id".amplitudeUserId(_:)
"amplitude_device_id".amplitudeDeviceId(_:)
"appmetrica_device_id".appmetricaDeviceId(_:)
"appmetrica_profile_id".appmetricaProfileId(_:)
"appsflyer_id".appsflyerId(_:)
"branch_id".branchId(_:)
"facebook_anonymous_id".facebookAnonymousId(_:)
"firebase_app_instance_id".firebaseAppInstanceId(_:)
"mixpanel_user_id".mixpanelUserId(_:)
"one_signal_subscription_id".oneSignalSubscriptionId(_:)
"one_signal_player_id".oneSignalPlayerId(_:)
"posthog_distinct_user_id".posthogDistinctUserId(_:)
"pushwoosh_hwid".pushwooshHWID(_:)
"tenjin_analytics_installation_id".tenjinAnalyticsInstallationId(_:)