# Adapty Documentation (Full Content) > Complete documentation content across all platforms. Generated on: 2026-03-05T16:27:48.720Z --- # ANDROID - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Generated on: 2026-03-05T16:27:47.920Z Total files: 41 --- # File: sdk-installation-android --- --- title: "Install & configure Android SDK" description: "Step-by-step guide on installing Adapty SDK on Android for subscription-based apps." --- Adapty SDK includes two key modules for seamless integration into your mobile app: - **Core Adapty**: This essential SDK is required for Adapty to function properly in your app. - **AdaptyUI**: This module is needed if you use the [Adapty Paywall Builder](adapty-paywall-builder), a user-friendly, no-code tool for easily creating cross-platform paywalls. AdaptyUI is automatically activated along with the core module. :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample app](https://github.com/adaptyteam/AdaptySDK-Android/tree/master/app), which demonstrates the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ## Requirements Minimum SDK requirement: `minSdkVersion 21` :::info Adapty is compatible with Google Play Billing Library up to 8.x. By default, Adapty works with Google Play Billing Library v.7.0.0 but, if you want to force a later version, you can manually [add the dependency](https://developer.android.com/google/play/billing/integrate#dependency). ::: ## Install Adapty SDK Choose your dependency setup method: - Standard Gradle: Add dependencies to your **module-level** `build.gradle` - If your project uses `.gradle.kts` files, add dependencies to your module-level `build.gradle.kts` - If you use version catalogs, add dependencies to your `libs.versions.toml` file and then, reference it in `build.gradle.kts` [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-Android.svg?style=flat&logo=android)](https://github.com/adaptyteam/AdaptySDK-Android/releases) ```groovy showLineNumbers dependencies { ... implementation platform('io.adapty:adapty-bom:') implementation 'io.adapty:android-sdk' // Only add this line if you plan to use Paywall Builder implementation 'io.adapty:android-ui' } ``` ```kotlin showLineNumbers dependencies { ... implementation(platform("io.adapty:adapty-bom:")) implementation("io.adapty:android-sdk") // Only add this line if you plan to use Paywall Builder: implementation("io.adapty:android-ui") } ``` ```toml showLineNumbers //libs.versions.toml [versions] .. adaptyBom = "" [libraries] .. adapty-bom = { module = "io.adapty:adapty-bom", version.ref = "adaptyBom" } adapty = { module = "io.adapty:android-sdk" } // Only add this line if you plan to use Paywall Builder: adapty-ui = { module = "io.adapty:android-ui" } //module-level build.gradle.kts dependencies { ... implementation(platform(libs.adapty.bom)) implementation(libs.adapty) // Only add this line if you plan to use Paywall Builder: implementation(libs.adapty.ui) } ``` If the dependency is not being resolved, please make sure that you have `mavenCentral()` in your Gradle scripts.
The instruction on how to add it If your project doesn't have `dependencyResolutionManagement` in your `settings.gradle`, add the following to your top-level `build.gradle` at the end of repositories: ```groovy showLineNumbers title="top-level build.gradle" allprojects { repositories { ... mavenCentral() } } ``` Otherwise, add the following to your `settings.gradle` in `repositories` of `dependencyResolutionManagement` section: ```groovy showLineNumbers title="settings.gradle" dependencyResolutionManagement { ... repositories { ... mavenCentral() } } ```
## Activate Adapty module of Adapty SDK ### Basic setup Activate the Adapty SDK in your app code. :::note The Adapty SDK only needs to be activated once in your app. ::: To get your **Public SDK Key**: 1. Go to Adapty Dashboard and navigate to [**App settings → General**](https://app.adapty.io/settings/general). 2. From the **Api keys** section, copy the **Public SDK Key** (NOT the Secret Key). 3. Replace `"YOUR_PUBLIC_SDK_KEY"` in the code. :::important - Make sure you use the **Public SDK key** for Adapty initialization, the **Secret key** should be used for [server-side API](getting-started-with-server-side-api) only. - **SDK keys** are unique for every app, so if you have multiple apps make sure you choose the right one. ::: ```kotlin showLineNumbers // In your Application class class MyApplication : Application() { override fun onCreate() { super.onCreate() Adapty.activate( applicationContext, AdaptyConfig.Builder("PUBLIC_SDK_KEY") .build() ) } } ``` ```java showLineNumbers // In your Application class public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); Adapty.activate( getApplicationContext(), new AdaptyConfig.Builder("PUBLIC_SDK_KEY") .build() ); } } ``` Now set up paywalls in your app: - If you use [Adapty Paywall Builder](adapty-paywall-builder), follow the [Paywall Builder quickstart](android-quickstart-paywalls). - If you build your own paywall UI, see the [quickstart for custom paywalls](android-quickstart-manual). ## Activate AdaptyUI module of Adapty SDK If you plan to use [Paywall Builder](adapty-paywall-builder.md), you need the AdaptyUI module. It is activated automatically when you activate the core module; you don't need to do anything else. ## Configure Proguard Before launching your app in the production, add `-keep class com.adapty.** { *; }` to your Proguard configuration. ## Optional setup ### Logging #### Set up the logging system Adapty logs errors and other important information to help you understand what is going on. There are the following levels available: | Level | Description | | :----------------------- | :------------------------------------------------------------------------------------------------------------------------ | | `AdaptyLogLevel.NONE` | Nothing will be logged. Default value | | `AdaptyLogLevel.ERROR` | Only errors will be logged | | `AdaptyLogLevel.WARN` | Errors and messages from the SDK that do not cause critical errors, but are worth paying attention to will be logged. | | `AdaptyLogLevel.INFO` | Errors, warnings, and various information messages will be logged. | | `AdaptyLogLevel.VERBOSE` | Any additional information that may be useful during debugging, such as function calls, API queries, etc. will be logged. | You can set the log level in your app before configuring Adapty. ```kotlin showLineNumbers Adapty.logLevel = AdaptyLogLevel.VERBOSE //recommended for development and the first production release ``` ```java showLineNumbers Adapty.setLogLevel(AdaptyLogLevel.VERBOSE); //recommended for development and the first production release ``` #### Redirect the logging system messages If you for some reason need to send messages from Adapty to your system or save them to a file, you can override the default behavior: ```kotlin showLineNumbers Adapty.setLogHandler { level, message -> //handle the log } ``` ```java showLineNumbers Adapty.setLogHandler((level, message) -> { //handle the log }); ``` ### Data policies Adapty doesn't store personal data of your users unless you explicitly send it, but you can implement additional data security policies to comply with the store or country guidelines. #### Disable IP address collection and sharing When activating the Adapty module, set `ipAddressCollectionDisabled` to `true` to disable user IP address collection and sharing. The default value is `false`. Use this parameter to enhance user privacy, comply with regional data protection regulations (like GDPR or CCPA), or reduce unnecessary data collection when IP-based features aren't required for your app. ```kotlin showLineNumbers AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withIpAddressCollectionDisabled(true) .build() ``` ```java showLineNumbers new AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withIpAddressCollectionDisabled(true) .build(); ``` #### Disable advertising ID (Ad ID) collection and sharing When activating the Adapty module, set `adIdCollectionDisabled` to `true` to disable the collection of the user [advertising ID](https://support.google.com/googleplay/android-developer/answer/6048248). The default value is `false`. Use this parameter to comply with Play Store policies, avoid triggering the advertising ID permission prompt, or if your app does not require advertising attribution or analytics based on Ad ID. ```kotlin showLineNumbers AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withAdIdCollectionDisabled(true) .build() ``` ```java showLineNumbers new AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withAdIdCollectionDisabled(true) .build(); ``` #### Set up media cache configuration for AdaptyUI By default, AdaptyUI caches media (such as images and videos) to improve performance and reduce network usage. You can customize the cache settings by providing a custom configuration. Use `AdaptyUI.configureMediaCache` to override the default cache size and validity period. This is optional—if you don't call this method, default values will be used (100MB disk size, 7 days validity). ```kotlin showLineNumbers val cacheConfig = MediaCacheConfiguration.Builder() .overrideDiskStorageSizeLimit(200L * 1024 * 1024) // 200 MB .overrideDiskCacheValidityTime(3.days) .build() AdaptyUI.configureMediaCache(cacheConfig) ``` ```java showLineNumbers MediaCacheConfiguration cacheConfig = new MediaCacheConfiguration.Builder() .overrideDiskStorageSizeLimit(200L * 1024 * 1024) // 200 MB .overrideDiskCacheValidityTime(TimeInterval.days(3)) .build(); AdaptyUI.configureMediaCache(cacheConfig); ``` **Parameters:** | Parameter | Presence | Description | |-------------------------|----------|-----------------------------------------------------------------------------| | diskStorageSizeLimit | optional | Total cache size on disk in bytes. Default is 100 MB. | | diskCacheValidityTime | optional | How long cached files are considered valid. Default is 7 days. | :::tip You can clear the media cache at runtime using `AdaptyUI.clearMediaCache(strategy)`, where `strategy` can be `CLEAR_ALL` or `CLEAR_EXPIRED_ONLY`. ::: ### Set obfuscated account IDs Google Play requires obfuscated account IDs for certain use cases to enhance user privacy and security. These IDs help Google Play identify purchases while keeping user information anonymous, which is particularly important for fraud prevention and analytics. You may need to set these IDs if your app handles sensitive user data or if you're required to comply with specific privacy regulations. The obfuscated IDs allow Google Play to track purchases without exposing actual user identifiers. ```kotlin showLineNumbers AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withObfuscatedAccountId("YOUR_OBFUSCATED_ACCOUNT_ID") .build() ``` ```java showLineNumbers new AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withObfuscatedAccountId("YOUR_OBFUSCATED_ACCOUNT_ID") .build(); ``` ### Run Adapty in a custom process By default, Adapty can only run in the main process of your app. If your app uses multiple processes, initialize Adapty only once; otherwise, unexpected behavior may occur. If you need to run Adapty in a different process, specify it in your configuration: ```kotlin showLineNumbers AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withProcessName(":custom") .build() ``` ```java showLineNumbers new AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withProcessName(":custom") .build(); ``` If you try to activate Adapty in another process without setting this value, the SDK will log a warning and skip activation. ### Enable local access levels By default, [local access levels](local-access-levels.md) are disabled on Android. To enable them, set `withLocalAccessLevelAllowed` to `true`: ```kotlin showLineNumbers AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withLocalAccessLevelAllowed(true) .build() ``` ```java showLineNumbers new AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withLocalAccessLevelAllowed(true) .build(); ``` ## Troubleshooting #### Android backup rules (Auto Backup configuration) Some SDKs (including Adapty) ship their own Android Auto Backup configuration. If you use multiple SDKs that define backup rules, the Android manifest merger can fail with an error mentioning `android:fullBackupContent`, `android:dataExtractionRules`, or `android:allowBackup`. Typical error symptoms: `Manifest merger failed: Attribute application@dataExtractionRules value=(@xml/sample_data_extraction_rules) is also present at [com.other.sdk:library:1.0.0] value=(@xml/other_sdk_data_extraction_rules)` To resolve this, you need to: - Tell the manifest merger to use your app’s values for backup-related attributes. - Merge backup rules from Adapty and other SDKs into a single XML file (or a pair of files for Android 12+). #### 1. Add the `tools` namespace to your manifest If not present yet, add the `tools` namespace to the root `` tag: ```xml ... ``` #### 2. Override backup attributes in `` In your app’s `AndroidManifest.xml`, update the `` tag so that your app provides the final values and tells the manifest merger to replace library values: ```xml ... ``` If any SDK also sets `android:allowBackup`, include it in `tools:replace` as well: ```xml tools:replace="android:allowBackup,android:fullBackupContent,android:dataExtractionRules" ``` #### 3. Create merged backup rules files Create XML files under `app/src/main/res/xml/` that combine Adapty's rules with rules from other SDKs. Android uses different backup rule formats depending on the OS version, so creating both files ensures compatibility across all Android versions your app supports. :::note The examples below show AppsFlyer as a sample third-party SDK. Replace or add rules for any other SDKs you're using in your app. ::: **For Android 12 and higher** (uses the new data extraction rules format): ```xml title="sample_data_extraction_rules.xml" ``` **For Android 11 and lower** (uses the legacy full backup content format): ```xml title="sample_backup_rules.xml" ``` With this setup: - Adapty’s backup exclusions (`AdaptySDKPrefs.xml`) are preserved. - Other SDKs’ exclusions (for example, `appsflyer-data`) are also applied. - The manifest merger uses your app’s configuration and no longer fails on conflicting backup attributes. #### Purchases fail after returning from another app If the Activity that starts the purchase flow uses a non-default `launchMode`, Android may recreate or reuse it incorrectly when the user returns from Google Play, a banking app, or a browser. This can cause the purchase result to be lost or treated as canceled. To ensure purchases work correctly, use only `standard` or `singleTop` launch modes for the Activity that starts the purchase flow, and avoid any other modes. In your `AndroidManifest.xml`, ensure the Activity that starts the purchase flow is set to `standard` or `singleTop`: ```xml ``` --- # File: android-quickstart-paywalls --- --- title: "Enable purchases by using paywalls in Android SDK" description: "Quickstart guide to setting up Adapty for in-app subscription management." --- To enable in-app purchases, you need to understand three key concepts: - [**Products**](product.md) – anything users can buy (subscriptions, consumables, lifetime access) - [**Paywalls**](paywalls.md) are configurations that define which products to offer. In Adapty, paywalls are the only way to retrieve products, but this design lets you modify offerings, pricing, and product combinations without touching your app code. - [**Placements**](placements.md) – where and when you show paywalls in your app (like `main`, `onboarding`, `settings`). You set up paywalls for placements in the dashboard, then request them by placement ID in your code. This makes it easy to run A/B tests and show different paywalls to different users. Adapty offers you three ways to enable purchases in your app. Select one of them depending on your app requirements: | Implementation | Complexity | When to use | |------------------------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Adapty Paywall Builder | ✅ Easy | You [create a complete, purchase-ready paywall in the no-code builder](quickstart-paywalls). Adapty automatically renders it and handles all the complex purchase flow, receipt validation, and subscription management behind the scenes. | | Manually created paywalls | 🟡 Medium | You implement your paywall UI in your app code, but still get the paywall object from Adapty to maintain flexibility in product offerings. See the [guide](android-quickstart-manual). | | Observer mode | 🔴 Hard | You already have your own purchase handling infrastructure and want to keep using it. Note that the observer mode has its limitations in Adapty. See the [article](observer-vs-full-mode). | :::important **The steps below show how to implement a paywall created in the Adapty paywall builder.** If you don't want to use the paywall builder, see the [guide for handling purchases in manually created paywalls](android-making-purchases.md). ::: To display a paywall created in the Adapty paywall builder, in your app code, you only need to: 1. **Get the paywall**: Get the paywall from Adapty. 2. **Display the paywall and Adapty will handle purchases for you**: Show the paywall container you've got in your app. 3. **Handle button actions**: Associate user interactions with the paywall with your app's response to them. For example, open links or close the paywall when users click buttons. ## Before you start Before you start, complete these steps: 1. [Connect your app to Google Play](initial-android) in the Adapty Dashboard. 2. [Create your products](create-product) in Adapty. 3. [Create a paywall and add products to it](create-paywall). 4. [Create a placement and add your paywall to it](create-placement). 5. [Install and activate the Adapty SDK](sdk-installation-android) in your app code. :::tip The fastest way to complete these steps is to follow the [quickstart guide](quickstart). ::: ## 1. Get the paywall Your paywalls are associated with placements configured in the dashboard. Placements allow you to run different paywalls for different audiences or to run [A/B tests](ab-tests.md). To get a paywall created in the Adapty paywall builder, you need to: 1. Get the `paywall` object by the [placement](placements.md) ID using the `getPaywall` method and check whether it is a paywall created in the builder. 2. Get the paywall view configuration using the `getViewConfiguration` method. The view configuration contains the UI elements and styling needed to display the paywall. :::important To get the view configuration, you must switch on the **Show on device** toggle in the Paywall Builder. Otherwise, you will get an empty view configuration, and the paywall won't be displayed. ::: ```kotlin showLineNumbers Adapty.getPaywall("YOUR_PLACEMENT_ID") { result -> if (result is AdaptyResult.Success) { val paywall = result.value if (!paywall.hasViewConfiguration) { return@getPaywall } AdaptyUI.getViewConfiguration(paywall) { configResult -> if (configResult is AdaptyResult.Success) { val viewConfiguration = configResult.value } } } } ``` ```java showLineNumbers Adapty.getPaywall("YOUR_PLACEMENT_ID", result -> { if (result instanceof AdaptyResult.Success) { AdaptyPaywall paywall = ((AdaptyResult.Success) result).getValue(); if (!paywall.hasViewConfiguration()) { return; } AdaptyUI.getViewConfiguration(paywall, configResult -> { if (configResult instanceof AdaptyResult.Success) { AdaptyUI.LocalizedViewConfiguration viewConfiguration = ((AdaptyResult.Success) configResult).getValue(); // use loaded configuration } }); } }); ``` ## 2. Display the paywall Now, when you have the paywall configuration, it's enough to add a few lines to display your paywall. In order to display the visual paywall on the device screen, you must first configure it. To do this, call the method `AdaptyUI.getPaywallView()` or create the `AdaptyPaywallView` directly: ```kotlin showLineNumbers val paywallView = AdaptyUI.getPaywallView( activity, viewConfiguration, null, // products = null means auto-fetch eventListener, ) ``` ```kotlin showLineNumbers val paywallView = AdaptyPaywallView(activity) // or retrieve it from xml ... with(paywallView) { showPaywall( viewConfiguration, null, // products = null means auto-fetch eventListener, ) } ``` ```java showLineNumbers AdaptyPaywallView paywallView = AdaptyUI.getPaywallView( activity, viewConfiguration, null, // products = null means auto-fetch eventListener, ); ``` ```java showLineNumbers AdaptyPaywallView paywallView = new AdaptyPaywallView(activity); //add to the view hierarchy if needed, or you receive it from xml ... paywallView.showPaywall(viewConfiguration, products, eventListener); ``` ```xml showLineNumbers ``` After the view has been successfully created, you can add it to the view hierarchy and display it on the screen of the device. :::tip For more details on how to display a paywall, see our [guide](android-present-paywalls.md). ::: ## 3. Handle button actions When users click buttons in the paywall, the Android SDK automatically handles purchases, restoration, closing the paywall, and opening links. However, other buttons have custom or pre-defined IDs and require handling actions in your code. Or, you may want to override their default behavior. For example, here is the default behavior for the close button. You don't need to add it in the code, but here, you can see how it is done if needed. :::tip Read our guides on how to handle button [actions](android-handle-paywall-actions.md) and [events](android-handling-events.md). ::: ```kotlin showLineNumbers title="Kotlin" override fun onActionPerformed(action: AdaptyUI.Action, context: Context) { when (action) { AdaptyUI.Action.Close -> (context as? Activity)?.onBackPressed() // default behavior } } ``` ```java showLineNumbers @Override public void onActionPerformed(@NonNull AdaptyUI.Action action, @NonNull Context context) { if (action instanceof AdaptyUI.Action.Close) { if (context instanceof Activity) { ((Activity) context).onBackPressed(); } } } ``` ## Next steps Your paywall is ready to be displayed in the app. [Test your purchases in Google Play Store](testing-on-android) to make sure you can complete a test purchase from the paywall. Now, you need to [check the users' access level](android-check-subscription-status.md) to ensure you display a paywall or give access to paid features to right users. ## Full example Here is how all those steps can be integrated in your app together. ```kotlin showLineNumbers title="Kotlin" class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Adapty.getPaywall("YOUR_PLACEMENT_ID") { paywallResult -> if (paywallResult is AdaptyResult.Success) { val paywall = paywallResult.value if (!paywall.hasViewConfiguration) { // Use custom logic return@getPaywall } AdaptyUI.getViewConfiguration(paywall) { configResult -> if (configResult is AdaptyResult.Success) { val viewConfiguration = configResult.value val paywallView = AdaptyUI.getPaywallView( this, viewConfiguration, null, // products = null means auto-fetch object : AdaptyUIEventListener { override fun onActionPerformed(action: AdaptyUI.Action, context: Context) { when (action) { is AdaptyUI.Action.Close -> { (context as? Activity)?.onBackPressed() } } } } ) setContentView(paywallView) } } } } } } ``` ```java showLineNumbers public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Adapty.getPaywall("YOUR_PLACEMENT_ID", paywallResult -> { if (paywallResult instanceof AdaptyResult.Success) { AdaptyPaywall paywall = ((AdaptyResult.Success) paywallResult).getValue(); if (!paywall.hasViewConfiguration()) { // Use custom logic return; } AdaptyUI.getViewConfiguration(paywall, configResult -> { if (configResult instanceof AdaptyResult.Success) { AdaptyUI.LocalizedViewConfiguration viewConfiguration = ((AdaptyResult.Success) configResult).getValue(); AdaptyPaywallView paywallView = AdaptyUI.getPaywallView( this, viewConfiguration, null, // products = null means auto-fetch new AdaptyUIEventListener() { @Override public void onActionPerformed(@NonNull AdaptyUI.Action action, @NonNull Context context) { if (action instanceof AdaptyUI.Action.Close) { if (context instanceof Activity) { ((Activity) context).onBackPressed(); } } } } ); setContentView(paywallView); } }); } }); } } ``` --- # File: android-check-subscription-status --- --- title: "Check subscription status in Android SDK" description: "Learn how to check subscription status in your Android app with Adapty." --- To decide whether users can access paid content or see a paywall, you need to check their [access level](access-level.md) in the profile. This article shows you how to access the profile state to decide what users need to see - whether to show them a paywall or grant access to paid features. ## Get subscription status When you decide whether to show a paywall or paid content to a user, you check their [access level](access-level.md) in their profile. You have two options: - Call `getProfile` if you need the latest profile data immediately (like on app launch) or want to force an update. - Set up **automatic profile updates** to keep a local copy that's automatically refreshed whenever the subscription status changes. ### Get profile The easiest way to get the subscription status is to use the `getProfile` method to access the profile: ```kotlin showLineNumbers Adapty.getProfile { result -> when (result) { is AdaptyResult.Success -> { val profile = result.value // check the access } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers Adapty.getProfile(result -> { if (result instanceof AdaptyResult.Success) { AdaptyProfile profile = ((AdaptyResult.Success) result).getValue(); // check the access } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` ### Listen to subscription updates To automatically receive profile updates in your app: 1. Use `Adapty.setOnProfileUpdatedListener()` to listen for profile changes - Adapty will automatically call this method whenever the user's subscription status changes. 2. Store the updated profile data when this method is called, so you can use it throughout your app without making additional network requests. ```kotlin class SubscriptionManager { private var currentProfile: AdaptyProfile? = null init { // Listen for profile updates Adapty.setOnProfileUpdatedListener { profile -> currentProfile = profile // Update UI, unlock content, etc. } } // Use stored profile instead of calling getProfile() fun hasAccess(): Boolean { return currentProfile?.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive == true } } ``` ```java public class SubscriptionManager { private AdaptyProfile currentProfile; public SubscriptionManager() { // Listen for profile updates Adapty.setOnProfileUpdatedListener(profile -> { this.currentProfile = profile; // Update UI, unlock content, etc. }); } // Use stored profile instead of calling getProfile() public boolean hasAccess() { if (currentProfile == null) { return false; } AdaptyAccessLevel premiumAccess = currentProfile.getAccessLevels().get("YOUR_ACCESS_LEVEL"); return premiumAccess != null && premiumAccess.isActive(); } } ``` :::note Adapty automatically calls the profile update listener when your app starts, providing cached subscription data even if the device is offline. ::: ## Connect profile with paywall logic When you need to make immediate decisions about showing paywalls or granting access to paid features, you can check the user's profile directly. This approach is useful for scenarios like app launch, when entering premium sections, or before displaying specific content. ```kotlin private fun initializePaywall() { loadPaywall { paywallView -> checkAccessLevel { result -> when (result) { is AdaptyResult.Success -> { if (!result.value && paywallView != null) { setContentView(paywallView) // Show paywall if no access } } is AdaptyResult.Error -> { if (paywallView != null) { setContentView(paywallView) // Show paywall if access check fails } } } } } } private fun checkAccessLevel(callback: ResultCallback) { Adapty.getProfile { result -> when (result) { is AdaptyResult.Success -> { val hasAccess = result.value.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive == true callback.onResult(AdaptyResult.Success(hasAccess)) } is AdaptyResult.Error -> { callback.onResult(AdaptyResult.Error(result.error)) } } } } ``` ```java private void initializePaywall() { loadPaywall(paywallView -> { checkAccessLevel(result -> { if (result instanceof AdaptyResult.Success) { boolean hasAccess = ((AdaptyResult.Success) result).getValue(); if (!hasAccess && paywallView != null) { setContentView(paywallView); // Show paywall if no access } } else if (result instanceof AdaptyResult.Error) { if (paywallView != null) { setContentView(paywallView); // Show paywall if access check fails } } }); }); } private void checkAccessLevel(ResultCallback callback) { Adapty.getProfile(result -> { if (result instanceof AdaptyResult.Success) { AdaptyProfile profile = ((AdaptyResult.Success) result).getValue(); AdaptyAccessLevel premiumAccess = profile.getAccessLevels().get("YOUR_ACCESS_LEVEL"); boolean hasAccess = premiumAccess != null && premiumAccess.isActive(); callback.onResult(AdaptyResult.success(hasAccess)); } else if (result instanceof AdaptyResult.Error) { callback.onResult(AdaptyResult.error(((AdaptyResult.Error) result).getError())); } }); } ``` ## Next steps Now, when you know how to track the subscription status, learn how to [work with user profiles](android-quickstart-identify.md) to ensure they can access what they have paid for. --- # File: android-quickstart-identify --- --- title: "Identify users in Android SDK" description: "Quickstart guide to setting up Adapty for in-app subscription management in Android." --- :::important This guide is for you if you have your own authentication system. Here, you will learn how to work with user profiles in Adapty to ensure it aligns with your existing authentication system. ::: How you manage users' purchases depends on your app's authentication model: - If your app doesn't use backend authentication and doesn't store user data, see the [section about anonymous users](#anonymous-users). - If your app has (or will have) backend authentication, see the [section about identified users](#identified-users). **Key concepts**: - **Profiles** are the entities required for the SDK to work. Adapty creates them automatically. - They can be anonymous **(without customer user ID)** or identified **(with customer user ID)**. - You provide **customer user ID** in order to cross-reference profiles in Adapty with your internal auth system Here is what is different for anonymous and identified users: | | Anonymous users | Identified users | |-------------------------|---------------------------------------------------|-------------------------------------------------------------------------| | **Purchase management** | Store-level purchase restoration | Maintain purchase history across devices through their customer user ID | | **Profile management** | New profiles on each reinstall | The same profile across sessions and devices | | **Data persistence** | Anonymous users' data is tied to app installation | Identified users' data persists across app installations | ## Anonymous users If you don't have backend authentication, **you don't need to handle authentication in the app code**: 1. When the SDK is activated on the app's first launch, Adapty **creates a new profile for the user**. 2. When the user purchases anything in the app, this purchase is **associated with their Adapty profile and their store account**. 3. When the user **re-installs** the app or installs it from a **new device**, Adapty **creates a new anonymous profile on activation**. 4. If the user has previously made purchases in your app, by default, their purchases are automatically synced from the App Store on the SDK activation. So, with anonymous users, new profiles will be created on each installation, but that's not a problem because, in the Adapty analytics, you can [configure what will be considered a new installation](general#4-installs-definition-for-analytics). For anonymous users, you need to count installs by **device IDs**. In this case, each app installation on a device is counted as an install, including reinstalls. ## Identified users You have two options to identify users in the app: - [**During login/signup:**](#during-loginsignup) If users sign in after your app starts, call `identify()` with a customer user ID when they authenticate. - [**During the SDK activation:**](#during-the-sdk-activation) If you already have a customer user ID stored when the app launches, send it when calling `activate()`. :::important By default, when Adapty receives a purchase from a Customer User ID that is currently associated with another Customer User ID, the access level is shared, so both profiles have paid access. You can configure this setting to transfer paid access from one profile to another or disable sharing completely. See the [article](general#6-sharing-purchases-between-user-accounts) for more details. ::: ### During login/signup If you're identifying users after the app launch (for example, after they log into your app or sign up), use the `identify` method to set their customer user ID. - If you **haven't used this customer user ID before**, Adapty will automatically link it to the current profile. - If you **have used this customer user ID to identify the user before**, Adapty will switch to working with the profile associated with this customer user ID. :::important Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one. ::: ```kotlin showLineNumbers Adapty.identify("YOUR_USER_ID") { error -> // Unique for each user if (error == null) { // successful identify } } ``` ```java showLineNumbers // User IDs must be unique for each user Adapty.identify("YOUR_USER_ID", error -> { if (error == null) { // successful identify } }); ``` ### During the SDK activation If you already know a customer user ID when you activate the SDK, you can send it in the `activate` method instead of calling `identify` separately. If you know a customer user ID but set it only after the activation, that will mean that, upon activation, Adapty will create a new anonymous profile and switch to the existing one only after you call `identify`. You can pass either an existing customer user ID (the one you have used before) or a new one. If you pass a new one, a new profile created upon activation will be automatically linked to the customer user ID. :::note By default, creating anonymous profiles does not affect analytics dashboards, because installs are counted based on device IDs. A device ID represents a single installation of the app from the store on a device and is regenerated only after the app is reinstalled. It does not depend on whether this is a first or repeated installation, or whether an existing customer user ID is used. Creating a profile (on SDK activation or logout), logging in, or upgrading the app without reinstalling the app does not generate additional install events. If you want to count installs based on unique users rather than devices, go to **App settings** and configure [**Installs definition for analytics**](general#4-installs-definition-for-analytics). ::: ```kotlin showLineNumbers AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withCustomerUserId("user123") // Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one. .build() ``` ```java showLineNumbers new AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withCustomerUserId("user123") // Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one. .build(); ``` ### Log users out If you have a button for logging users out, use the `logout` method. :::important Logging users out creates a new anonymous profile for the user. ::: ```kotlin showLineNumbers Adapty.logout { error -> if (error == null) { // successful logout } } ``` ```java showLineNumbers Adapty.logout(error -> { if (error == null) { // successful logout } }); ``` :::info To log users back into the app, use the `identify` method. ::: ### Allow purchases without login If your users can make purchases both before and after they log into your app, you need to ensure that they will keep access after they log in: 1. When a logged-out user makes a purchase, Adapty ties it to their anonymous profile ID. 2. When the user logs into their account, Adapty switches to working with their identified profile. - If it is a new customer user ID (e.g., the purchase has been made before registration), Adapty assigns the customer user ID to the current profile, so all the purchase history is maintained. - If it is an existing customer user ID (the customer user ID is already linked to a profile), you need to get the actual access level after the profile switch. You can either call [`getProfile`](android-check-subscription-status.md) right after the identification, or [listen for profile updates](android-check-subscription-status.md) so the data syncs automatically. ## Next steps Congratulations! You have implemented in-app payment logic in your app! We wish you all the best with your app monetization! To get even more from Adapty, you can explore these topics: - [**Testing**](troubleshooting-test-purchases.md): Ensure that everything works as expected - [**Onboardings**](android-onboardings.md): Engage users with onboardings and drive retention - [**Integrations**](configuration.md): Integrate with marketing attribution and analytics services in just one line of code - [**Set custom profile attributes**](android-setting-user-attributes.md): Add custom attributes to user profiles and create segments, so you can launch A/B tests or show different paywalls to different users --- # File: adapty-cursor-android --- --- title: "Integrate Adapty into your Android app with AI assistance" description: "A step-by-step guide to integrating Adapty into your Android app using Cursor, Context7, ChatGPT, Claude, or other AI tools." --- This guide helps you integrate Adapty into your Android app with the help of an LLM. You'll start by preparing your Adapty dashboard, then work through each implementation stage by sending focused doc links to your LLM. At the end, you'll find best practices for setting up your AI tools with Adapty documentation. :::tip Copy this entire page as Markdown and paste it into your LLM to get started — click **Copy for LLM** at the top of the page or open [the .md version](https://adapty.io/docs/adapty-cursor-android.md). The LLM will use the guide links and checkpoints to walk you through each stage. ::: ## Before you start: dashboard checklist Adapty requires dashboard configuration before you write any SDK code. Your LLM cannot look up dashboard values for you — you'll need to provide them. ### Required before coding 1. **Connect your app store**: In the Adapty Dashboard, go to **App settings → General**. This is required for purchases to work. [Connect Google Play](integrate-payments.md) 2. **Copy your Public SDK key**: In the Adapty Dashboard, go to **App settings → General**, then find the **API keys** section. In code, this is the string you pass to the Adapty configuration builder. 3. **Create at least one product**: In the Adapty Dashboard, go to the **Products** page. You don't reference products directly in code — Adapty delivers them through paywalls. [Add products](quickstart-products.md) 4. **Create a paywall and a placement**: In the Adapty Dashboard, create a paywall on the **Paywalls** page, then assign it to a placement on the **Placements** page. In code, the placement ID is the string you pass to `Adapty.getPaywall("YOUR_PLACEMENT_ID")`. [Create paywall](quickstart-paywalls.md) 5. **Set up access levels**: In the Adapty Dashboard, configure per product on the **Products** page. In code, the string checked in `profile.accessLevels["premium"]?.isActive`. The default `premium` access level works for most apps. If paying users get access to different features depending on the product (for example, a `basic` plan vs. a `pro` plan), [create additional access levels](assigning-access-level-to-a-product.md) before you start coding. :::tip Once you have all five, you're ready to write code. Tell your LLM: "My Public SDK key is X, my placement ID is Y" so it can generate correct initialization and paywall-fetching code. ::: ### Set up when ready These are not required to start coding, but you'll want them as your integration matures: - **A/B tests**: Configure on the **Placements** page. No code change needed. [A/B tests](ab-tests.md) - **Additional paywalls and placements**: Add more `getPaywall` calls with different placement IDs. - **Analytics integrations**: Configure on the **Integrations** page. Setup varies by integration. See [analytics integrations](analytics-integration.md) and [attribution integrations](attribution-integration.md). ## Feed Adapty docs to your LLM ### Use Context7 (recommended) [Context7](https://context7.com) is an MCP server that gives your LLM direct access to up-to-date Adapty documentation. Your LLM fetches the right docs automatically based on what you ask — no manual URL pasting needed. Context7 works with **Cursor**, **Claude Code**, **Windsurf**, and other MCP-compatible tools. To set it up, run: ``` npx ctx7 setup ``` This detects your editor and configures the Context7 server. For manual setup, see the [Context7 GitHub repository](https://github.com/upstash/context7). Once configured, reference the Adapty library in your prompts: ``` Use the adaptyteam/adapty-docs library to look up how to install the Android SDK ``` :::warning Even though Context7 removes the need to paste doc links manually, the implementation order matters. Follow the [implementation walkthrough](#implementation-walkthrough) below step by step to make sure everything works. ::: ### Use plain text docs You can access any Adapty doc as plain text Markdown. Add `.md` to the end of its URL, or click **Copy for LLM** under the article title. For example: [adapty-cursor-android.md](https://adapty.io/docs/adapty-cursor-android.md). Each stage in the [implementation walkthrough](#implementation-walkthrough) below includes a "Send this to your LLM" block with `.md` links to paste. For more documentation at once, see [index files and platform-specific subsets](#plain-text-doc-index-files) below. ## Implementation walkthrough The rest of this guide walks through Adapty integration in implementation order. Each stage includes the docs to send to your LLM, what you should see when done, and common issues. ### Plan your integration Before jumping into code, ask your LLM to analyze your project and create an implementation plan. If your AI tool supports a planning mode (like Cursor's or Claude Code's plan mode), use it so the LLM can read both your project structure and the Adapty docs before writing any code. Tell your LLM which approach you use for purchases — this affects the guides it should follow: - [**Adapty Paywall Builder**](adapty-paywall-builder.md): You create paywalls in Adapty's no-code builder, and the SDK renders them automatically. - [**Manually created paywalls**](android-making-purchases.md): You build your own paywall UI in code but still use Adapty to fetch products and handle purchases. - [**Observer mode**](observer-vs-full-mode.md): You keep your existing purchase infrastructure and use Adapty only for analytics and integrations. Not sure which one to pick? Read the [comparison table in the quickstart](android-quickstart-paywalls.md). ### Install and configure the SDK Add the Adapty SDK dependency via Gradle in Android Studio and activate it with your Public SDK key. This is the foundation — nothing else works without it. **Guide:** [Install & configure Adapty SDK](sdk-installation-android.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/sdk-installation-android.md ``` :::tip[Checkpoint] - **Expected:** App builds and runs. Logcat shows Adapty activation log. - **Gotcha:** "Public API key is missing" → check you replaced the placeholder with your real key from App settings. ::: ### Show paywalls and handle purchases Fetch a paywall by placement ID, display it, and handle purchase events. The guides you need depend on how you handle purchases. Test each purchase in the sandbox as you go — don't wait until the end. See [Test purchases in sandbox](test-purchases-in-sandbox.md) for setup instructions. **Guides:** - [Enable purchases using paywalls (quickstart)](android-quickstart-paywalls.md) - [Fetch Paywall Builder paywalls and their configuration](android-get-pb-paywalls.md) - [Display paywalls](android-present-paywalls.md) - [Handle paywall events](android-handling-events.md) - [Respond to button actions](android-handle-paywall-actions.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/android-quickstart-paywalls.md - https://adapty.io/docs/android-get-pb-paywalls.md - https://adapty.io/docs/android-present-paywalls.md - https://adapty.io/docs/android-handling-events.md - https://adapty.io/docs/android-handle-paywall-actions.md ``` :::tip[Checkpoint] - **Expected:** Paywall appears with your configured products. Tapping a product triggers the sandbox purchase dialog. - **Gotcha:** Empty paywall or `getPaywall` error → verify placement ID matches the dashboard exactly and the placement has an audience assigned. ::: **Guides:** - [Enable purchases in your custom paywall (quickstart)](android-quickstart-manual.md) - [Fetch paywalls and products](fetch-paywalls-and-products-android.md) - [Render paywall designed by remote config](present-remote-config-paywalls-android.md) - [Make purchases](android-making-purchases.md) - [Restore purchases](android-restore-purchase.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/android-quickstart-manual.md - https://adapty.io/docs/fetch-paywalls-and-products-android.md - https://adapty.io/docs/present-remote-config-paywalls-android.md - https://adapty.io/docs/android-making-purchases.md - https://adapty.io/docs/android-restore-purchase.md ``` :::tip[Checkpoint] - **Expected:** Your custom paywall displays products fetched from Adapty. Tapping a product triggers the sandbox purchase dialog. - **Gotcha:** Empty products array → verify the paywall has products assigned in the dashboard and the placement has an audience. ::: **Guides:** - [Observer mode overview](observer-vs-full-mode.md) - [Implement Observer mode](implement-observer-mode-android.md) - [Report transactions in Observer mode](report-transactions-observer-mode-android.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/observer-vs-full-mode.md - https://adapty.io/docs/implement-observer-mode-android.md - https://adapty.io/docs/report-transactions-observer-mode-android.md ``` :::tip[Checkpoint] - **Expected:** After a sandbox purchase using your existing purchase flow, the transaction appears in the Adapty dashboard **Event Feed**. - **Gotcha:** No events → verify you're reporting transactions to Adapty and Google Play Real-Time Developer Notifications are configured. ::: ### Check subscription status After a purchase, check the user profile for an active access level to gate premium content. **Guide:** [Check subscription status](android-check-subscription-status.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/android-check-subscription-status.md ``` :::tip[Checkpoint] - **Expected:** After a sandbox purchase, `profile.accessLevels["premium"]?.isActive` returns `true`. - **Gotcha:** Empty `accessLevels` after purchase → check the product has an access level assigned in the dashboard. ::: ### Identify users Link your app user accounts to Adapty profiles so purchases persist across devices. :::important Skip this step if your app has no authentication. ::: **Guide:** [Identify users](android-quickstart-identify.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/android-quickstart-identify.md ``` :::tip[Checkpoint] - **Expected:** After calling `Adapty.identify("your-user-id")`, the dashboard **Profiles** section shows your custom user ID. - **Gotcha:** Call `identify` after activation but before fetching paywalls to avoid anonymous profile attribution. ::: ### Prepare for release Once your integration works in the sandbox, walk through the release checklist to make sure everything is production-ready. **Guide:** [Release checklist](release-checklist.md) Send this to your LLM: ``` Read these Adapty docs before releasing: - https://adapty.io/docs/release-checklist.md ``` :::tip[Checkpoint] - **Expected:** All checklist items confirmed: store connection, server notifications, purchase flow, access level checks, and privacy requirements. - **Gotcha:** Missing Google Play Real-Time Developer Notifications → configure them in **App settings → Android SDK** or events won't appear in the dashboard. ::: ## Plain text doc index files If you need to give your LLM broader context beyond individual pages, we host index files that list or combine all Adapty documentation: - [`llms.txt`](https://adapty.io/docs/llms.txt): Lists all pages with `.md` links. An [emerging standard](https://llmstxt.org/) for making websites accessible to LLMs. Note that for some AI agents (e.g., ChatGPT) you will need to download `llms.txt` and upload it to the chat as a file. - [`llms-full.txt`](https://adapty.io/docs/llms-full.txt): The entire Adapty documentation site combined into a single file. Very large — use only when you need the full picture. - Android-specific [`android-llms.txt`](https://adapty.io/docs/android-llms.txt) and [`android-llms-full.txt`](https://adapty.io/docs/android-llms-full.txt): Platform-specific subsets that save tokens compared to the full site. --- # File: android-get-pb-paywalls --- --- title: "Fetch Paywall Builder paywalls and their configuration in Android SDK" description: "Learn how to retrieve PB paywalls in Adapty for better subscription control in your Android app." --- After [you designed the visual part for your paywall](adapty-paywall-builder) with the new Paywall Builder in the Adapty Dashboard, you can display it in your mobile app. The first step in this process is to get the paywall associated with the placement and its view configuration as described below. :::warning The new Paywall Builder works with Android SDK version 3.0 or higher. For presenting paywalls in Adapty SDK v2 designed with the legacy Paywall Builder, see [Display paywalls designed with legacy Paywall Builder](android-present-paywalls-legacy.md). ::: Please be aware that this topic refers to Paywall Builder-customized paywalls. If you are implementing your paywalls manually, please refer to the [Fetch paywalls and products for remote config paywalls in your mobile app](fetch-paywalls-and-products-android) topic. :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. :::
Before you start displaying paywalls in your mobile app (click to expand) 1. [Create your products](create-product) in the Adapty Dashboard. 2. [Create a paywall and incorporate the products into it](create-paywall) in the Adapty Dashboard. 3. [Create placements and incorporate your paywall into it](create-placement) in the Adapty Dashboard. 4. Install [Adapty SDK](sdk-installation-android) in your mobile app.
## Fetch paywall designed with Paywall Builder If you've [designed a paywall using the Paywall Builder](adapty-paywall-builder), you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. Nevertheless, you need to get its ID via the placement, its view configuration, and then present it in your mobile app. To ensure optimal performance, it's crucial to retrieve the paywall and its [view configuration](android-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) as early as possible, allowing sufficient time for images to download before presenting them to the user. To get a paywall, use the `getPaywall` method: ```kotlin showLineNumbers ... Adapty.getPaywall("YOUR_PLACEMENT_ID", locale = "en", loadTimeout = 10.seconds) { result -> when (result) { is AdaptyResult.Success -> { val paywall = result.value // the requested paywall } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers ... Adapty.getPaywall("YOUR_PLACEMENT_ID", "en", TimeInterval.seconds(10), result -> { if (result instanceof AdaptyResult.Success) { AdaptyPaywall paywall = ((AdaptyResult.Success) result).getValue(); // the requested paywall } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` Parameters: | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the [paywall localization](add-paywall-locale-in-adapty-paywall-builder). This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.

| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

Adapty SDK stores paywalls locally in two layers: regularly updated cache described above and [fallback paywalls](fallback-paywalls). We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.

| | **loadTimeout** | default: 5 sec |

This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.

Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.

For Android: You can create `TimeInterval` with extension functions (like `5.seconds`, where `.seconds` is from `import com.adapty.utils.seconds`), or `TimeInterval.seconds(5)`. To set no limitation, use `TimeInterval.INFINITE`.

| Response parameters: | Parameter | Description | | :-------- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------| | Paywall | An [`AdaptyPaywall`](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall/) object with a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch the view configuration of paywall designed using Paywall Builder :::important Make sure to enable the **Show on device** toggle in the paywall builder. If this option isn't turned on, the view configuration won't be available to retrieve. ::: After fetching the paywall, check if it includes a `ViewConfiguration`, which indicates that it was created using Paywall Builder. This will guide you on how to display the paywall. If the `ViewConfiguration` is present, treat it as a Paywall Builder paywall; if not, [handle it as a remote config paywall](present-remote-config-paywalls). Use the `getViewConfiguration` method to load the view configuration. ```kotlin showLineNumbers if (!paywall.hasViewConfiguration) { // use your custom logic return } AdaptyUI.getViewConfiguration(paywall, loadTimeout = 10.seconds) { result -> when(result) { is AdaptyResult.Success -> { val viewConfiguration = result.value // use loaded configuration } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` | Parameter | Presence | Description | | :-------------- | :------------- | :----------------------------------------------------------- | | **paywall** | required | An `AdaptyPaywall` object to obtain a controller for the desired paywall. | | **loadTimeout** | default: 5 sec | This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood. | Use the `getViewConfiguration` method to load the view configuration. ```java showLineNumbers if (!paywall.hasViewConfiguration()) { // use your custom logic return; } AdaptyUI.getViewConfiguration(paywall, TimeInterval.seconds(10), result -> { if (result instanceof AdaptyResult.Success) { AdaptyUI.LocalizedViewConfiguration viewConfiguration = ((AdaptyResult.Success) result).getValue(); // use loaded configuration } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` | Parameter | Presence | Description | | :----------------------- | :------------- | :----------------------------------------------------------- | | **paywall** | required | An `AdaptyPaywall` object to obtain a controller for the desired paywall. | | **loadTimeout** | default: 5 sec | This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood. | :::note If you are using multiple languages, learn how to add a [Paywall Builder localization](add-paywall-locale-in-adapty-paywall-builder) and how to use locale codes correctly [here](android-localizations-and-locale-codes). ::: Once loaded, [present the paywall](android-present-paywalls). ## Get a paywall for a default audience to fetch it faster Typically, paywalls are fetched almost instantly, so you don’t need to worry about speeding up this process. However, in cases where you have numerous audiences and paywalls, and your users have a weak internet connection, fetching a paywall may take longer than you'd like. In such situations, you might want to display a default paywall to ensure a smooth user experience rather than showing no paywall at all. To address this, you can use the `getPaywallForDefaultAudience` method, which fetches the paywall of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the paywall by the `getPaywall` method, as detailed in the [Fetch Paywall Information](#fetch-paywall-designed-with-paywall-builder) section above. :::warning Why we recommend using `getPaywall` The `getPaywallForDefaultAudience` method comes with a few significant drawbacks: - **Potential backward compatibility issues**: If you need to show different paywalls for different app versions (current and future), you may face challenges. You’ll either have to design paywalls that support the current (legacy) version or accept that users with the current (legacy) version might encounter issues with non-rendered paywalls. - **Loss of targeting**: All users will see the same paywall designed for the **All Users** audience, which means you lose personalized targeting (including based on countries, marketing attribution or your own custom attributes). If you're willing to accept these drawbacks to benefit from faster paywall fetching, use the `getPaywallForDefaultAudience` method as follows. Otherwise stick to `getPaywall` described [above](#fetch-paywall-designed-with-paywall-builder). ::: ```kotlin showLineNumbers Adapty.getPaywallForDefaultAudience("YOUR_PLACEMENT_ID", locale = "en") { result -> when (result) { is AdaptyResult.Success -> { val paywall = result.value // the requested paywall } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers Adapty.getPaywallForDefaultAudience("YOUR_PLACEMENT_ID", "en", result -> { if (result instanceof AdaptyResult.Success) { AdaptyPaywall paywall = ((AdaptyResult.Success) result).getValue(); // the requested paywall } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` :::note The `getPaywallForDefaultAudience` method is available starting from Android SDK 2.11.3 ::: | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the [Placement](placements). This is the value you specified when creating a placement in your Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.

| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

| ## Customize assets To customize images and videos in your paywall, implement the custom assets. Hero images and videos have predefined IDs: `hero_image` and `hero_video`. In a custom asset bundle, you target these elements by their IDs and customize their behavior. For other images and videos, you need to [set a custom ID](https://adapty.io/docs/custom-media) in the Adapty dashboard. For example, you can: - Show a different image or video to some users. - Show a local preview image while a remote main image is loading. - Show a preview image before running a video. :::important To use this feature, update the Adapty Android SDK to version 3.7.0 or higher. ::: Here’s an example of how you can provide custom asssets via a simple dictionary: ```kotlin showLineNumbers val customAssets = AdaptyCustomAssets.of( "hero_image" to AdaptyCustomImageAsset.remote( url = "https://example.com/image.jpg", preview = AdaptyCustomImageAsset.file( FileLocation.fromAsset("images/hero_image_preview.png"), ) ), "hero_video" to AdaptyCustomVideoAsset.file( FileLocation.fromResId(requireContext(), R.raw.custom_video), preview = AdaptyCustomImageAsset.file( FileLocation.fromResId(requireContext(), R.drawable.video_preview), ), ), ) val paywallView = AdaptyUI.getPaywallView( activity, viewConfiguration, products, eventListener, insets, customAssets, ) ``` :::note If an asset is not found, the paywall will fall back to its default appearance. ::: --- # File: android-present-paywalls --- --- title: "Android - Present new Paywall Builder paywalls" description: "Learn how to present paywalls on Android for effective monetization." --- If you've customized a paywall using the Paywall Builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. :::warning This guide is for **new Paywall Builder paywalls** only which require SDK v3.0. The process for presenting paywalls differs for paywalls designed with different versions of Paywall Builde, remote config paywalls, and [Observer mode](observer-vs-full-mode). - For presenting **Legacy Paywall Builder paywalls**, check out [Android- Present legacy Paywall Builder paywalls](android-present-paywalls-legacy). - For presenting **Remote config paywalls**, see [Render paywall designed by remote config](present-remote-config-paywalls). - For presenting **Observer mode paywalls**, see [Android - Present Paywall Builder paywalls in Observer mode](android-present-paywall-builder-paywalls-in-observer-mode) ::: To get the `viewConfiguration` object used below, see [Fetch Paywall Builder paywalls and their configuration](android-get-pb-paywalls). In order to display the visual paywall on the device screen, you must first configure it. To do this, call the method `AdaptyUI.getPaywallView()` or create the `AdaptyPaywallView` directly: ```kotlin showLineNumbers val paywallView = AdaptyUI.getPaywallView( activity, viewConfiguration, products, eventListener, insets, personalizedOfferResolver, tagResolver, timerResolver, ) ``` ```kotlin showLineNumbers val paywallView = AdaptyPaywallView(activity) // or retrieve it from xml ... with(paywallView) { showPaywall( viewConfiguration, products, eventListener, insets, personalizedOfferResolver, tagResolver, timerResolver, ) } ``` ```java showLineNumbers AdaptyPaywallView paywallView = AdaptyUI.getPaywallView( activity, viewConfiguration, products, eventListener, insets, personalizedOfferResolver, tagResolver, timerResolver ); ``` ```java showLineNumbers AdaptyPaywallView paywallView = new AdaptyPaywallView(activity); //add to the view hierarchy if needed, or you receive it from xml ... paywallView.showPaywall(viewConfiguration, products, eventListener, insets, personalizedOfferResolver, tagResolver, timerResolver); ``` ```xml showLineNumbers ``` After the view has been successfully created, you can add it to the view hierarchy and display it on the screen of the device. If you get `AdaptyPaywallView` _not_ by calling `AdaptyUI.getPaywallView()`, you will also need to call the `.showPaywall()` method. In order to display the visual paywall on the device screen, you must first configure it. To do this, use this composable function: ```kotlin showLineNumbers AdaptyPaywallScreen( viewConfiguration, products, eventListener, insets, personalizedOfferResolver, tagResolver, timerResolver, ) ``` Request parameters: | Parameter | Presence | Description | | :---------------------------- | :------- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **viewConfiguration** | required | Supply an `AdaptyUI.LocalizedViewConfiguration` object containing visual details of the paywall. Use the `Adapty.getViewConfiguration(paywall)` method to load it. Refer to [Fetch the visual configuration of paywall](android-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) topic for more details. | | **products** | optional | Provide an array of `AdaptyPaywallProduct `to optimize the display timing of products on the screen. If `null` is passed, AdaptyUI will automatically fetch the required products. | | **eventListener** | optional | Provide an `AdaptyUiEventListener` to observe paywall events. Extending AdaptyUiDefaultEventListener is recommended for ease of use. Refer to [Handling paywall events](android-handling-events) topic for more details. | | **insets** | optional |

Insets are the spaces around the paywall that prevent tapable elements from getting hidden behind system bars.

Default: `UNSPECIFIED` which means Adapty will automatically adjust the insets, which works great for edge-to-edge paywalls.

If your paywall isn’t edge-to-edge, you might want to set custom insets. For how to do that, read in the [Change paywall insets](android-present-paywalls#change-paywall-insets) section below.

| | **personalizedOfferResolver** | optional | To indicate personalized pricing ([read more](https://developer.android.com/google/play/billing/integrate#personalized-price) ), implement `AdaptyUiPersonalizedOfferResolver` and pass your own logic that maps `AdaptyPaywallProduct` to true if the product's price is personalized, otherwise false. | | **tagResolver** | optional | Use `AdaptyUiTagResolver` to resolve custom tags within the paywall text. This resolver takes a tag parameter and resolves it to a corresponding string. Refer to [Custom tags in Paywall Builder](custom-tags-in-paywall-builder) topic for more details. | | **timerResolver** | optional | Pass the resolver here if you are going to use custom timer functionality. | :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ## Change paywall insets Insets are the spaces around the paywall that prevent tapable elements from getting hidden behind system bars. By default, Adapty will automatically adjust the insets, which works great for edge-to-edge paywalls. If your paywall isn’t edge-to-edge, you might want to set custom insets: - If neither the status bar nor the navigation bar overlap with the `AdaptyPaywallView`, use `AdaptyPaywallInsets.NONE`. - For more custom setups, like if your paywall overlaps with the top status bar but not the bottom, you can set only the `bottomInset` to `0`, as shown in the example below: ```kotlin showLineNumbers //create extension function fun View.onReceiveSystemBarsInsets(action: (insets: Insets) -> Unit) { ViewCompat.setOnApplyWindowInsetsListener(this) { _, insets -> val systemBarInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) ViewCompat.setOnApplyWindowInsetsListener(this, null) action(systemBarInsets) insets } } //and then use it with the view paywallView.onReceiveSystemBarsInsets { insets -> val paywallInsets = AdaptyPaywallInsets.vertical(insets.top, 0) paywallView.showPaywall( viewConfiguration, products, eventListener, paywallInsets, personalizedOfferResolver, tagResolver, timerResolver, ) } ``` ```java showLineNumbers ... ViewCompat.setOnApplyWindowInsetsListener(paywallView, (view, insets) -> { Insets systemBarInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()); ViewCompat.setOnApplyWindowInsetsListener(paywallView, null); AdaptyPaywallInsets paywallInsets = AdaptyPaywallInsets.of(systemBarInsets.top, 0); paywallView.showPaywall(paywall, products, viewConfiguration, paywallInsets, productTitleResolver); return insets; }); ``` ## Use developer-defined timer To use developer-defined timers in your mobile app, create a `timerResolver` object—a dictionary or map that pairs custom timers with the string values that will replace them when the paywall is rendered. Here's an example: ```kotlin showLineNumbers ... val customTimers = mapOf( "CUSTOM_TIMER_NY" to Calendar.getInstance(TimeZone.getDefault()).apply { set(2025, 0, 1) }.time, // New Year 2025 ) val timerResolver = AdaptyUiTimerResolver { timerId -> customTimers.getOrElse(timerId, { Date(System.currentTimeMillis() + 3600 * 1000L) /* in 1 hour */ } ) } ``` ```JAVA showLineNumbers ... Map customTimers = new HashMap<>(); customTimers.put( "CUSTOM_TIMER_NY", new Calendar.Builder().setTimeZone(TimeZone.getDefault()).setDate(2025, 0, 1).build().getTime() ); AdaptyUiTimerResolver timerResolver = new AdaptyUiTimerResolver() { @NonNull @Override public Date timerEndAtDate(@NonNull String timerId) { Date date = customTimers.get(timerId); return date != null ? date : new Date(System.currentTimeMillis() + 3600 * 1000L); /* in 1 hour */ } }; ``` In this example, `CUSTOM_TIMER_NY` is the **Timer ID** of the developer-defined timer you set in the Adapty dashboard. The `timerResolver` ensures your app dynamically updates the timer with the correct value—like `13d 09h 03m 34s` (calculated as the timer’s end time, such as New Year’s Day, minus the current time). ## Use custom tags To use custom tags in your mobile app, create a `tagResolver` object—a dictionary or map that pairs custom tags with the string values that will replace them when the paywall is rendered. Here's an example: ```kotlin showLineNumbers val customTags = mapOf("USERNAME" to "John") val tagResolver = AdaptyUiTagResolver { tag -> customTags[tag] } ``` ```java showLineNumbers Map customTags = new HashMap<>(); customTags.put("USERNAME", "John"); AdaptyUiTagResolver tagResolver = customTags::get; ``` In this example, `USERNAME` is a custom tag you entered in the Adapty dashboard as ``. The `tagResolver` ensures that your app dynamically replaces this custom tag with the specified value—like `John`. We recommend creating and populating the `tagResolver` right before presenting your paywall. Once it's ready, pass it to the AdaptyUI method you use for presenting the paywall. ## Change paywall loading indicator color You can override the default color of the loading indicator in the following way: ```XML showLineNumbers title = "XML" ``` --- # File: android-handle-paywall-actions --- --- title: "Respond to button actions in Android SDK" description: "Handle paywall button actions in Android using Adapty for better app monetization." --- If you are building paywalls using the Adapty paywall builder, it's crucial to set up buttons properly: 1. Add a [button in the paywall builder](paywall-buttons.md) and assign it either a pre-existing action or create a custom action ID. 2. Write code in your app to handle each action you've assigned. This guide shows how to handle custom and pre-existing actions in your code. :::warning **Only purchases, restorations, paywall closures, and URL opening are handled automatically.** All other button actions require proper response implementation in the app code. ::: ## Close paywalls To add a button that will close your paywall: 1. In the paywall builder, add a button and assign it the **Close** action. 2. In your app code, implement a handler for the `close` action that dismisses the paywall. :::info In the Android SDK, the `close` action triggers closing the paywall by default. However, you can override this behavior in your code if needed. For example, closing one paywall might trigger opening another. ::: ```kotlin override fun onActionPerformed(action: AdaptyUI.Action, context: Context) { when (action) { AdaptyUI.Action.Close -> (context as? Activity)?.onBackPressed() // default behavior } } ``` ## Open URLs from paywalls :::tip If you want to add a group of links (e.g., terms of use and purchase restoration), add a **Link** element in the paywall builder and handle it the same way as buttons with the **Open URL** action. ::: To add a button that opens a link from your paywall (e.g., **Terms of use** or **Privacy policy**): 1. In the paywall builder, add a button, assign it the **Open URL** action, and enter the URL you want to open. 2. In your app code, implement a handler for the `openUrl` action that opens the received URL in a browser. :::info In the Android SDK, the `openUrl` action triggers opening the URL by default. However, you can override this behavior in your code if needed. ::: ```kotlin override fun onActionPerformed(action: AdaptyUI.Action, context: Context) { when (action) { is AdaptyUI.Action.OpenUrl -> { val intent = Intent(Intent.ACTION_VIEW, Uri.parse(action.url)) // default behavior context.startActivity(intent) } } } ``` ## Log into the app To add a button that logs users into your app: 1. In the paywall builder, add a button and assign it the **Login** action. 2. In your app code, implement a handler for the `login` action that identifies your user. ```kotlin override fun onActionPerformed(action: AdaptyUI.Action, context: Context) { when (action) { AdaptyUI.Action.Login -> { val intent = Intent(context, LoginActivity::class.java) context.startActivity(intent) } } } ``` ## Handle custom actions To add a button that handles any other actions: 1. In the paywall builder, add a button, assign it the **Custom** action, and assign it an ID. 2. In your app code, implement a handler for the action ID you've created. For example, if you have another set of subscription offers or one-time purchases, you can add a button that will display another paywall: ```kotlin override fun onActionPerformed(action: AdaptyUI.Action, context: Context) { when (action) { is AdaptyUI.Action.Custom -> { if (action.customId == "openNewPaywall") { // Display another paywall } } } } ``` --- # File: android-handling-events --- --- title: "Android - Handle paywall events" description: "Handle Android subscription events efficiently with Adapty's event tracking tools." --- :::important This guide covers event handling for purchases, restorations, product selection, and paywall rendering. You must also implement button handling (closing paywall, opening links, etc.). See our [guide on handling button actions](android-handle-paywall-actions.md) for details. ::: Paywalls configured with the [Paywall Builder](adapty-paywall-builder) don't need extra code to make and restore purchases. However, they generate some events that your app can respond to. Those events include button presses (close buttons, URLs, product selections, and so on) as well as notifications on purchase-related actions taken on the paywall. Learn how to respond to these events below. :::warning This guide is for **new Paywall Builder paywalls** only which require Adapty SDK v3.0 or later. For presenting paywalls in Adapty SDK v2 designed with legacy Paywall Builder, see [Handle paywall events designed with legacy Paywall Builder](android-handling-events-legacy). ::: :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: If you need to control or monitor the processes that take place on the purchase screen, implement the `AdaptyUiEventListener` methods. If you would like to leave the default behavior in some cases, you can extend `AdaptyUiDefaultEventListener` and override only those methods you want to change. Below are the defaults from `AdaptyUiDefaultEventListener`. ### User-generated events #### Product selection If a product is selected for purchase (by a user or by the system), this method will be invoked: ```kotlin showLineNumbers title="Kotlin" public override fun onProductSelected( product: AdaptyPaywallProduct, context: Context, ) {} ```
Event example (Click to expand) ```javascript { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } ```
#### Started purchase If a user initiates the purchase process, this method will be invoked: ```kotlin showLineNumbers title="Kotlin" public override fun onPurchaseStarted( product: AdaptyPaywallProduct, context: Context, ) {} ```
Event example (Click to expand) ```javascript { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } ```
The method will not be invoked in Observer mode. Refer to the [Android - Present Paywall Builder paywalls in Observer mode](android-present-paywall-builder-paywalls-in-observer-mode) topic for details. #### Successful, canceled, or pending purchase If purchase succeeds, this method will be invoked: ```kotlin showLineNumbers title="Kotlin" public override fun onPurchaseFinished( purchaseResult: AdaptyPurchaseResult, product: AdaptyPaywallProduct, context: Context, ) { if (purchaseResult !is AdaptyPurchaseResult.UserCanceled) context.getActivityOrNull()?.onBackPressed() } ```
Event examples (Click to expand) ```javascript // Successful purchase { "purchaseResult": { "type": "Success", "profile": { "accessLevels": { "premium": { "id": "premium", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } } } }, "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } // Cancelled purchase { "purchaseResult": { "type": "UserCanceled" }, "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } // Pending purchase { "purchaseResult": { "type": "Pending" }, "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } ```
We recommend dismissing the screen in that case. The method will not be invoked in Observer mode. Refer to the [Android - Present Paywall Builder paywalls in Observer mode](android-present-paywall-builder-paywalls-in-observer-mode) topic for details. #### Failed purchase If a purchase fails due to an error, this method will be invoked. This includes Google Play Billing errors (payment restrictions, invalid products, network failures), transaction verification failures, and system errors. Note that user cancellations trigger `onPurchaseFinished` with a cancelled result instead, and pending payments do not trigger this method. ```kotlin showLineNumbers title="Kotlin" public override fun onPurchaseFailure( error: AdaptyError, product: AdaptyPaywallProduct, context: Context, ) {} ```
Event example (Click to expand) ```javascript { "error": { "code": "purchase_failed", "message": "Purchase failed due to insufficient funds", "details": { "underlyingError": "Insufficient funds in account" } }, "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } ```
The method will not be invoked in Observer mode. Refer to the [Android - Present Paywall Builder paywalls in Observer mode](android-present-paywall-builder-paywalls-in-observer-mode) topic for details. #### Finished web payment navigation This method is invoked after an attempt to open a [web paywall](web-paywall.md) for a specific product. This includes both successful and failed navigation attempts: ```kotlin showLineNumbers title="Kotlin" public override fun onFinishWebPaymentNavigation( product: AdaptyPaywallProduct?, error: AdaptyError?, context: Context, ) {} ``` **Parameters:** | Parameter | Description | |:------------|:---------------------------------------------------------------------------------------------------| | **product** | An `AdaptyPaywallProduct` for which the web paywall was opened. Can be `null`. | | **error** | An `AdaptyError` object if the web paywall navigation failed; `null` if navigation was successful. |
Event examples (Click to expand) ```javascript // Successful navigation { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "error": null } // Failed navigation { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "error": { "code": "web_navigation_failed", "message": "Failed to open web paywall", "details": { "underlyingError": "Browser unavailable" } } } ```
#### Successful restore If restoring a purchase succeeds, this method will be invoked: ```kotlin showLineNumbers title="Kotlin" public override fun onRestoreSuccess( profile: AdaptyProfile, context: Context, ) {} ```
Event example (Click to expand) ```javascript { "profile": { "accessLevels": { "premium": { "id": "premium", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } }, "subscriptions": [ { "vendorProductId": "premium_monthly", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } ] } } ```
We recommend dismissing the screen if the user has the required `accessLevel`. Refer to the [Subscription status](android-listen-subscription-changes.md) topic to learn how to check it. #### Failed restore If `Adapty.restorePurchases()` fails, this method will be invoked: ```kotlin showLineNumbers title="Kotlin" public override fun onRestoreFailure( error: AdaptyError, context: Context, ) {} ```
Event example (Click to expand) ```javascript { "error": { "code": "restore_failed", "message": "Purchase restoration failed", "details": { "underlyingError": "No previous purchases found" } } } ```
#### Upgrade subscription When a user attempts to purchase a new subscription while another subscription is active, you can control how the new purchase should be handled by overriding this method. You have two options: 1. **Replace the current subscription** with the new one: ```kotlin showLineNumbers title="Kotlin" public override fun onAwaitingPurchaseParams( product: AdaptyPaywallProduct, context: Context, onPurchaseParamsReceived: AdaptyUiEventListener.PurchaseParamsCallback, ): AdaptyUiEventListener.PurchaseParamsCallback.IveBeenInvoked { onPurchaseParamsReceived( AdaptyPurchaseParameters.Builder() .withSubscriptionUpdateParams(AdaptySubscriptionUpdateParameters(...)) .build() ) return AdaptyUiEventListener.PurchaseParamsCallback.IveBeenInvoked } ``` 2. **Keep both subscriptions** (add the new one separately): ```kotlin showLineNumbers title="Kotlin" public override fun onAwaitingPurchaseParams( product: AdaptyPaywallProduct, context: Context, onPurchaseParamsReceived: AdaptyUiEventListener.PurchaseParamsCallback, ): AdaptyUiEventListener.PurchaseParamsCallback.IveBeenInvoked { onPurchaseParamsReceived(AdaptyPurchaseParameters.Empty) return AdaptyUiEventListener.PurchaseParamsCallback.IveBeenInvoked } ``` :::note If you don't override this method, the default behavior is to keep both subscriptions active (equivalent to using `AdaptyPurchaseParameters.Empty`). ::: You can also set additional purchase parameters if needed: ```kotlin AdaptyPurchaseParameters.Builder() .withSubscriptionUpdateParams(AdaptySubscriptionUpdateParameters(...)) // optional - for replacing current subscription .withOfferPersonalized(true) // optional - if using personalized pricing .build() ``` If a new subscription is purchased while another is still active, override this method to replace the current one with the new one. If the active subscription should remain active and the new one is added separately, call `onSubscriptionUpdateParamsReceived(null)`: ```kotlin showLineNumbers title="Kotlin" public override fun onAwaitingSubscriptionUpdateParams( product: AdaptyPaywallProduct, context: Context, onSubscriptionUpdateParamsReceived: SubscriptionUpdateParamsCallback, ) { onSubscriptionUpdateParamsReceived(AdaptySubscriptionUpdateParameters(...)) } ```
Event example (Click to expand) ```javascript { "product": { "vendorProductId": "premium_yearly", "localizedTitle": "Premium Yearly", "localizedDescription": "Premium subscription for 1 year", "localizedPrice": "$99.99", "price": 99.99, "currencyCode": "USD" }, "subscriptionUpdateParams": { "replacementMode": "with_time_proration" } } ```
### Data fetching and rendering #### Product loading errors If you don't pass the products during the initialization, AdaptyUI will retrieve the necessary objects from the server by itself. If this operation fails, AdaptyUI will report the error by invoking this method: ```kotlin showLineNumbers title="Kotlin" public override fun onLoadingProductsFailure( error: AdaptyError, context: Context, ): Boolean = false ```
Event example (Click to expand) ```javascript { "error": { "code": "products_loading_failed", "message": "Failed to load products from the server", "details": { "underlyingError": "Network timeout" } } } ```
If you return `true`, AdaptyUI will repeat the request in 2 seconds. #### Rendering errors If an error occurs during the interface rendering, it will be reported by calling this method: ```kotlin showLineNumbers title="Kotlin" public override fun onRenderingError( error: AdaptyError, context: Context, ) {} ```
Event example (Click to expand) ```javascript { "error": { "code": "rendering_failed", "message": "Failed to render paywall interface", "details": { "underlyingError": "Invalid paywall configuration" } } } ```
In a normal situation, such errors should not occur, so if you come across one, please let us know. --- # File: android-use-fallback-paywalls --- --- title: "Android - Use fallback paywalls" description: "Handle cases when users are offline or Adapty servers aren't available." --- :::warning Fallback paywalls are supported by Android SDK v2.11 and later. ::: To maintain a fluid user experience, it is important to set up [fallbacks](/fallback-paywalls) for your [paywalls](paywalls) and [onboardings](onboardings). This precaution extends the application's capabilities in case of partial or complete loss of internet connection. * **If the application cannot access Adapty servers:** It will be able to display a fallback paywall, and access the local onboarding configuration. * **If the application cannot access the internet:** It will be able to display a fallback paywall. Onboardings include remote content and require an internet connection to function. :::important Before you follow the steps in this guide, [download](/local-fallback-paywalls) the fallback configuration files from Adapty. ::: ## Configuration 1. Move the fallback configuration file to the `assets` or `res/raw` directory of your Android project. 2. Call the `.setFallback` method **before** you fetch the target paywall or onboarding. ```kotlin showLineNumbers //if you put the 'android_fallback.json' file to the 'assets' directory val location = FileLocation.fromAsset("android_fallback.json") //or `FileLocation.fromAsset("/android_fallback.json")` if you placed it in a child folder of 'assets') //if you put the 'android_fallback.json' file to the 'res/raw' directory val location = FileLocation.fromResId(context, R.raw.android_fallback) //you can also pass a file URI val fileUri: Uri = //get Uri for the file with fallback paywalls val location = FileLocation.fromFileUri(fileUri) //pass the file location Adapty.setFallback(location, callback) ``` ```java showLineNumbers //if you put the 'android_fallback.json' file to the 'assets' directory FileLocation location = FileLocation.fromAsset("android_fallback.json"); //or `FileLocation.fromAsset("/android_fallback.json");` if you placed it in a child folder of 'assets') //if you put the 'android_fallback.json' file to the 'res/raw' directory FileLocation location = FileLocation.fromResId(context, R.raw.android_fallback); //you can also pass a file URI Uri fileUri = //get Uri for the file with fallback paywalls FileLocation location = FileLocation.fromFileUri(fileUri); //pass the file location Adapty.setFallback(location, callback); ``` Parameters: | Parameter | Description | | :----------- | :----------------------------------------------------------- | | **location** | The [FileLocation](https://android.adapty.io/adapty/com.adapty.utils/-file-location/-companion/) object for the fallback configuration file | --- # File: android-localizations-and-locale-codes --- --- title: "Use localizations and locale codes in Android SDK" description: "Manage app localizations and locale codes to reach a global audience (Android)." --- ## Why this is important There are a few scenarios when locale codes come into play — for example, when you're trying to fetch the correct paywall for the current localization of your app. As locale codes are complicated and can vary from platform to platform, we rely on an internal standard for all the platforms we support. However, because these codes are complicated, it is really important for you to understand what exactly are you sending to our server to get the correct localization, and what happens next — so you will always receive what you expect. ## Locale code standard at Adapty For locale codes, Adapty uses a slightly modified [BCP 47 standard](https://en.wikipedia.org/wiki/IETF_language_tag): every code consists of lowercase subtags, separated by hyphens. Some examples: `en` (English), `pt-br` (Portuguese (Brazil)), `zh` (Simplified Chinese), `zh-hant` (Traditional Chinese). ## Locale code matching When Adapty receives a call from the client-side SDK with the locale code and starts looking for a corresponding localization of a paywall, the following happens: 1. The incoming locale string is converted to lowercase and all the underscores (`_`) are replaced with hyphens (`-`) 2. We then look for the localization with the fully matching locale code 3. If no match was found, we take the substring before the first hyphen (`pt` for `pt-br`) and look for the matching localization 4. If no match was found again, we return the default `en` localization This way an iOS device that sent `'pt_BR'`, an Android device that sent `pt-BR`, and another device that sent `pt-br` will get the same result. ## Implementing localizations: recommended way If you're wondering about localizations, chances are you're already dealing with the localized string files in your project. If that's the case, we recommend placing some key-value with the intended Adapty locale code in each of your files for the corresponding localizations. And then extract the value for this key when calling our SDK, like so: ```kotlin showLineNumbers // 1. Modify your strings.xml files /* strings.xml - Spanish */ es /* strings.xml - Portuguese (Brazil) */ pt-br // 2. Extract and use the locale code val localeCode = context.getString(R.string.adapty_paywalls_locale) // pass locale code to AdaptyUI.getViewConfiguration or Adapty.getPaywall method ``` That way you can ensure you're in full control of what localization will be retrieved for every user of your app. ## Implementing localizations: the other way You can get similar (but not identical) results without explicitly defining locale codes for every localization. That would mean extracting a locale code from some other objects that your platform provides, like this: ```kotlin showLineNumbers val locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) context.resources.configuration.locales[0] else context.resources.configuration.locale val localeCode = locale.toLanguageTag() // pass locale code to AdaptyUI.getViewConfiguration or Adapty.getPaywall method ``` Note that we don't recommend this approach because it's hard to predict what exactly will Adapty's server get. Should you decide to use this approach anyway — make sure you've covered all the relevant use cases. --- # File: android-web-paywall --- --- title: "Implement web paywalls in Android SDK" description: "Set up a web paywall to get paid without the Play Store fees and audits." --- :::important Before you begin, make sure you have [configured your web paywall in the dashboard](web-paywall.md) and installed Adapty SDK version 3.15 or later. ::: ## Open web paywalls If you are working with a paywall you developed yourself, you need to handle web paywalls using the SDK method. The `.openWebPaywall` method: 1. Generates a unique URL allowing Adapty to link a specific paywall shown to a particular user to the web page they are redirected to. 2. Tracks when your users return to the app and then requests `.getProfile` at short intervals to determine whether the profile access rights have been updated. This way, if the payment has been successful and access rights have been updated, the subscription activates in the app almost immediately. :::note After users return to the app, refresh the UI to reflect the profile updates. Adapty will receive and process profile update events. ::: ```kotlin showLineNumbers Adapty.openWebPaywall( activity = activity, product = product, ) { error -> if (error == null) { // the web paywall was opened successfully } else { // handle the error } } ``` :::note There are two versions of the `openWebPaywall` method: 1. `openWebPaywall(product)` that generates URLs by paywall and adds the product data to URLs as well. 2. `openWebPaywall(paywall)` that generates URLs by paywall without adding the product data to URLs. Use it when your products in the Adapty paywall differ from those in the web paywall. ::: ## Open web paywalls in an in-app browser By default, web paywalls open in the external browser. To provide a seamless user experience, you can open web paywalls in an in-app browser. This displays the web purchase page within your application, allowing users to complete transactions without switching apps. To enable this, set the `presentation` parameter to `AdaptyWebPresentation.InAppBrowser`: ```kotlin showLineNumbers Adapty.openWebPaywall( activity = activity, product = product, presentation = AdaptyWebPresentation.InAppBrowser, ) { error -> if (error == null) { // the web paywall was opened successfully } else { // handle the error val adaptyError = error } } ``` --- # File: android-troubleshoot-paywall-builder --- --- title: "Troubleshoot Paywall Builder in Android SDK" description: "Troubleshoot Paywall Builder in Android SDK" --- This guide helps you resolve common issues when using paywalls designed in the Adapty Paywall Builder in the Android SDK. ## Getting a paywall configuration fails **Issue**: The `getViewConfiguration` method fails to retrieve paywall configuration. **Reason**: The paywall is not enabled for device display in the Paywall Builder. **Solution**: Enable the **Show on device** toggle in the Paywall Builder. ## The paywall view number is too big **Issue**: The paywall view count is showing double the expected number. **Reason**: You may be calling `logShowPaywall` in your code, which duplicates the view count if you're using the Paywall builder. For paywalls designed with the Paywall Builder, analytics are tracked automatically, so you don't need to use this method. **Solution**: Ensure you are not calling `logShowPaywall` in your code if you're using the Paywall builder. ## Other issues **Issue**: You're experiencing other Paywall Builder-related problems not covered above. **Solution**: Migrate the SDK to the latest version using the [migration guides](android-sdk-migration-guides) if needed. Many issues are resolved in newer SDK versions. --- # File: android-quickstart-manual --- --- title: "Enable purchases in your custom paywall in Android SDK" description: "Integrate Adapty SDK into your custom Android paywalls to enable in-app purchases." --- This guide describes how to integrate Adapty into your custom paywalls. Keep full control over paywall implementation, while the Adapty SDK fetches products, handles new purchases, and restores previous ones. :::important **This guide is for developers who are implementing custom paywalls.** If you want the easiest way to enable purchases, use the [Adapty Paywall Builder](android-quickstart-paywalls.md). With Paywall Builder, you create paywalls in a no-code visual editor, Adapty handles all purchase logic automatically, and you can test different designs without republishing your app. ::: ## Before you start ### Set up products To enable in-app purchases, you need to understand three key concepts: - [**Products**](product.md) – anything users can buy (subscriptions, consumables, lifetime access) - [**Paywalls**](paywalls.md) – configurations that define which products to offer. In Adapty, paywalls are the only way to retrieve products, but this design lets you modify products, prices, and offers without touching your app code. - [**Placements**](placements.md) – where and when you show paywalls in your app (like `main`, `onboarding`, `settings`). You set up paywalls for placements in the dashboard, then request them by placement ID in your code. This makes it easy to run A/B tests and show different paywalls to different users. Make sure you understand these concepts even if you work with your custom paywall. Basically, they are just your way to manage the products you sell in your app. To implement your custom paywall, you will need to create a **paywall** and add it to a **placement**. This setup allows you to retrieve your products. To understand what you need to do in the dashboard, follow the quickstart guide [here](quickstart.md). ### Manage users You can work either with or without backend authentication on your side. However, the Adapty SDK handles anonymous and identified users differently. Read the [identification quickstart guide](android-quickstart-identify.md) to understand the specifics and ensure you are working with users properly. ## Step 1. Get products To retrieve products for your custom paywall, you need to: 1. Get the `paywall` object by passing [placement](placements.md) ID to the `getPaywall` method. 2. Get the products array for this paywall using the `getPaywallProducts` method. ```kotlin showLineNumbers fun loadPaywall() { Adapty.getPaywall("YOUR_PLACEMENT_ID") { result -> when (result) { is AdaptyResult.Success -> { val paywall = result.value Adapty.getPaywallProducts(paywall) { productResult -> when (productResult) { is AdaptyResult.Success -> { val products = productResult.value // Use products to build your custom paywall UI } is AdaptyResult.Error -> { val error = productResult.error // Handle the error } } } } is AdaptyResult.Error -> { val error = result.error // Handle the error } } } } ``` ```java showLineNumbers public void loadPaywall() { Adapty.getPaywall("YOUR_PLACEMENT_ID", result -> { if (result instanceof AdaptyResult.Success) { AdaptyPaywall paywall = ((AdaptyResult.Success) result).getValue(); Adapty.getPaywallProducts(paywall, productResult -> { if (productResult instanceof AdaptyResult.Success) { List products = ((AdaptyResult.Success>) productResult).getValue(); // Use products to build your custom paywall UI } else if (productResult instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) productResult).getError(); // Handle the error } }); } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // Handle the error } }); } ``` ## Step 2. Accept purchases When a user taps on a product in your custom paywall, call the `makePurchase` method with the selected product. This will handle the purchase flow and return the updated profile. ```kotlin showLineNumbers fun purchaseProduct(activity: Activity, product: AdaptyPaywallProduct) { Adapty.makePurchase(activity, product) { result -> when (result) { is AdaptyResult.Success -> { when (val purchaseResult = result.value) { is AdaptyPurchaseResult.Success -> { val profile = purchaseResult.profile // Purchase successful, profile updated } is AdaptyPurchaseResult.UserCanceled -> { // User canceled the purchase } is AdaptyPurchaseResult.Pending -> { // Purchase is pending (e.g., user will pay offline with cash) } } } is AdaptyResult.Error -> { val error = result.error // Handle the error } } } } ``` ```java showLineNumbers public void purchaseProduct(Activity activity, AdaptyPaywallProduct product) { Adapty.makePurchase(activity, product, null, result -> { if (result instanceof AdaptyResult.Success) { AdaptyPurchaseResult purchaseResult = ((AdaptyResult.Success) result).getValue(); if (purchaseResult instanceof AdaptyPurchaseResult.Success) { AdaptyProfile profile = ((AdaptyPurchaseResult.Success) purchaseResult).getProfile(); // Purchase successful, profile updated } else if (purchaseResult instanceof AdaptyPurchaseResult.UserCanceled) { // User canceled the purchase } else if (purchaseResult instanceof AdaptyPurchaseResult.Pending) { // Purchase is pending (e.g., user will pay offline with cash) } } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // Handle the error } }); } ``` ## Step 3. Restore purchases Google Play and other app stores require all apps with subscriptions to provide a way users can restore their purchases. Call the `restorePurchases` method when the user taps the restore button. This will sync their purchase history with Adapty and return the updated profile. ```kotlin showLineNumbers fun restorePurchases() { Adapty.restorePurchases { result -> when (result) { is AdaptyResult.Success -> { val profile = result.value // Restore successful, profile updated } is AdaptyResult.Error -> { val error = result.error // Handle the error } } } } ``` ```java showLineNumbers public void restorePurchases() { Adapty.restorePurchases(result -> { if (result instanceof AdaptyResult.Success) { AdaptyProfile profile = ((AdaptyResult.Success) result).getValue(); // Restore successful, profile updated } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // Handle the error } }); } ``` ## Next steps Your paywall is ready to be displayed in the app. [Test your purchases in Google Play Store](testing-on-android) to make sure you can complete a test purchase from the paywall. To see how this works in a production-ready implementation, check out the [ProductListFragment.kt](https://github.com/adaptyteam/AdaptySDK-Android/blob/master/app/src/main/java/com/adapty/example/ProductListFragment.kt) in our example app, which demonstrates purchase handling with proper error handling, UI feedback, and subscription management. Next, [check whether users have completed their purchase](android-check-subscription-status.md) to determine whether to display the paywall or grant access to paid features. --- # File: fetch-paywalls-and-products-android --- --- title: "Fetch paywalls and products for remote config paywalls in Android SDK" description: "Fetch paywalls and products in Adapty Android SDK to enhance user monetization." --- Before showcasing remote config and custom paywalls, you need to fetch the information about them. Please be aware that this topic refers to remote config and custom paywalls. For guidance on fetching paywalls for Paywall Builder-customized paywalls, please consult [Fetch Paywall Builder paywalls and their configuration](android-get-pb-paywalls). :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. :::
Before you start fetching paywalls and products in your mobile app (click to expand) 1. [Create your products](create-product) in the Adapty Dashboard. 2. [Create a paywall and incorporate the products into your paywall](create-paywall) in the Adapty Dashboard. 3. [Create placements and incorporate your paywall into the placement](create-placement) in the Adapty Dashboard. 4. [Install Adapty SDK](sdk-installation-android) in your mobile app.
## Fetch paywall information In Adapty, a [product](product) serves as a combination of products from both the App Store and Google Play. These cross-platform products are integrated into paywalls, enabling you to showcase them within specific mobile app placements. To display the products, you need to obtain a [Paywall](paywalls) from one of your [placements](placements) with `getPaywall` method. :::important **Don't hardcode product IDs.** The only ID you should hardcode is the placement ID. Paywalls are configured remotely, so the number of products and available offers can change at any time. Your app must handle these changes dynamically—if a paywall returns two products today and three tomorrow, display all of them without code changes. ::: ```kotlin showLineNumbers Adapty.getPaywall("YOUR_PLACEMENT_ID", locale = "en") { result -> when (result) { is AdaptyResult.Success -> { val paywall = result.value // the requested paywall } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers Adapty.getPaywall("YOUR_PLACEMENT_ID", "en", result -> { if (result instanceof AdaptyResult.Success) { AdaptyPaywall paywall = ((AdaptyResult.Success) result).getValue(); // the requested paywall } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the [Placement](placements). This is the value you specified when creating a placement in your Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

See [Localizations and locale codes](android-localizations-and-locale-codes) for more information on locale codes and how we recommend using them.

| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

Adapty SDK stores paywalls in two layers: regularly updated cache described above and [fallback paywalls](android-use-fallback-paywalls) . We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.

| | **loadTimeout** | default: 5 sec |

This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.

Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.

| Don't hardcode product IDs! Since paywalls are configured remotely, the available products, the number of products, and special offers (such as free trials) can change over time. Make sure your code handles these scenarios. For example, if you initially retrieve 2 products, your app should display those 2 products. However, if you later retrieve 3 products, your app should display all 3 without requiring any code changes. The only thing you have to hardcode is placement ID. Response parameters: | Parameter | Description | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall/) object with: a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch products Once you have the paywall, you can query the product array that corresponds to it: ```kotlin showLineNumbers Adapty.getPaywallProducts(paywall) { result -> when (result) { is AdaptyResult.Success -> { val products = result.value // the requested products } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers Adapty.getPaywallProducts(paywall, result -> { if (result instanceof AdaptyResult.Success) { List products = ((AdaptyResult.Success>) result).getValue(); // the requested products } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` Response parameters: | Parameter | Description | | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Products | List of [`AdaptyPaywallProduct`](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall-product/) objects with: product identifier, product name, price, currency, subscription length, and several other properties. | When implementing your own paywall design, you will likely need access to these properties from the [`AdaptyPaywallProduct`](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall-product/) object. Illustrated below are the most commonly used properties, but refer to the linked document for full details on all available properties. | Property | Description | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Title** | To display the title of the product, use `product.localizedTitle`. Note that the localization is based on the users' selected store country rather than the locale of the device itself. | | **Price** | To display a localized version of the price, use `product.price.localizedString`. This localization is based on the locale info of the device. You can also access the price as a number using `product.price.amount`. The value will be provided in the local currency. To get the associated currency symbol, use `product.price.currencySymbol`. | | **Subscription Period** | To display the period (e.g. week, month, year, etc.), use `product.subscriptionDetails?.localizedSubscriptionPeriod`. This localization is based on the locale of the device. To fetch the subscription period programmatically, use `product.subscriptionDetails?.subscriptionPeriod`. From there you can access the `unit` enum to get the length (i.e. DAY, WEEK, MONTH, YEAR, or UNKNOWN). The `numberOfUnits` value will get you the number of period units. For example, for a quarterly subscription, you'd see `MONTH` in the unit property, and `3` in the numberOfUnits property. | | **Introductory Offer** | To display a badge or other indicator that a subscription contains an introductory offer, check out the `product.subscriptionDetails?.introductoryOfferPhases` property. This is a list that can contain up to two discount phases: the free trial phase and the introductory price phase. Within each phase object are the following helpful properties:
• `paymentMode`: an enum with values `FREE_TRIAL`, `PAY_AS_YOU_GO`, `PAY_UPFRONT`, and `UNKNOWN`. Free trials will be the `FREE_TRIAL` type.
• `price`: The discounted price as a number. For free trials, look for `0` here.
• `localizedNumberOfPeriods`: a string localized using the device's locale describing the length of the offer. For example, a three day trial offer shows `3 days` in this field.
• `subscriptionPeriod`: Alternatively, you can get the individual details of the offer period with this property. It works in the same manner for offers as the previous section describes.
• `localizedSubscriptionPeriod`: A formatted subscription period of the discount for the user's locale. | ## Speed up paywall fetching with default audience paywall Typically, paywalls are fetched almost instantly, so you don't need to worry about speeding up this process. However, in cases where you have numerous audiences and paywalls, and your users have a weak internet connection, fetching a paywall may take longer than you'd like. In such situations, you might want to display a default paywall to ensure a smooth user experience rather than showing no paywall at all. To address this, you can use the `getPaywallForDefaultAudience` method, which fetches the paywall of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the paywall by the `getPaywall` method, as detailed in the [Fetch Paywall Information](fetch-paywalls-and-products-android#fetch-paywall-information) section above. :::warning Why we recommend using `getPaywall` The `getPaywallForDefaultAudience` method comes with a few significant drawbacks: - **Potential backward compatibility issues**: If you need to show different paywalls for different app versions (current and future), you may face challenges. You'll either have to design paywalls that support the current (legacy) version or accept that users with the current (legacy) version might encounter issues with non-rendered paywalls. - **Loss of targeting**: All users will see the same paywall designed for the **All Users** audience, which means you lose personalized targeting (including based on countries, marketing attribution or your own custom attributes). If you're willing to accept these drawbacks to benefit from faster paywall fetching, use the `getPaywallForDefaultAudience` method as follows. Otherwise, stick to the `getPaywall` described [above](fetch-paywalls-and-products-android#fetch-paywall-information). ::: ```kotlin showLineNumbers Adapty.getPaywallForDefaultAudience("YOUR_PLACEMENT_ID", locale = "en") { result -> when (result) { is AdaptyResult.Success -> { val paywall = result.value // the requested paywall } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers Adapty.getPaywallForDefaultAudience("YOUR_PLACEMENT_ID", "en", result -> { if (result instanceof AdaptyResult.Success) { AdaptyPaywall paywall = ((AdaptyResult.Success) result).getValue(); // the requested paywall } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` :::note The `getPaywallForDefaultAudience` method is available starting from Android SDK version 2.11.3. ::: | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the [Placement](placements). This is the value you specified when creating a placement in your Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

See [Localizations and locale codes](android-localizations-and-locale-codes) for more information on locale codes and how we recommend using them.

| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

| --- # File: present-remote-config-paywalls-android --- --- title: "Render paywall designed by remote config in Android SDK" description: "Discover how to present remote config paywalls in Adapty Android SDK to personalize user experience." --- If you've customized a paywall using remote config, you'll need to implement rendering in your mobile app's code to display it to users. Since remote config offers flexibility tailored to your needs, you're in control of what's included and how your paywall view appears. We provide a method for fetching the remote configuration, giving you the autonomy to showcase your custom paywall configured via remote config. ## Get paywall remote config and present it To get a remote config of a paywall, access the `remoteConfig` property and extract the needed values. ```kotlin showLineNumbers Adapty.getPaywall("YOUR_PLACEMENT_ID") { result -> when (result) { is AdaptyResult.Success -> { val paywall = result.value val headerText = paywall.remoteConfig?.dataMap?.get("header_text") as? String } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers Adapty.getPaywall("YOUR_PLACEMENT_ID", result -> { if (result instanceof AdaptyResult.Success) { AdaptyPaywall paywall = ((AdaptyResult.Success) result).getValue(); AdaptyPaywall.RemoteConfig remoteConfig = paywall.getRemoteConfig(); if (remoteConfig != null) { if (remoteConfig.getDataMap().get("header_text") instanceof String) { String headerText = (String) remoteConfig.getDataMap().get("header_text"); } } } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` At this point, once you've received all the necessary values, it's time to render and assemble them into a visually appealing page. Ensure that the design accommodates various mobile phone screens and orientations, providing a seamless and user-friendly experience across different devices. :::warning Make sure to [record the paywall view event](present-remote-config-paywalls-android#track-paywall-view-events) as described below, allowing Adapty analytics to capture information for funnels and A/B tests. ::: After you've done with displaying the paywall, continue with setting up a purchase flow. When the user makes a purchase, simply call `.makePurchase()` with the product from your paywall. For details on the`.makePurchase()` method, read [Making purchases](android-making-purchases). We recommend [creating a backup paywall called a fallback paywall](android-use-fallback-paywalls). This backup will display to the user when there's no internet connection or cache available, ensuring a smooth experience even in these situations. ## Track paywall view events Adapty assists you in measuring the performance of your paywalls. While we gather data on purchases automatically, logging paywall views needs your input because only you know when a customer sees a paywall. To log a paywall view event, simply call `.logShowPaywall(paywall)`, and it will be reflected in your paywall metrics in funnels and A/B tests. :::important Calling `.logShowPaywall(paywall)` is not needed if you are displaying paywalls created in the [paywall builder](adapty-paywall-builder.md). ::: ```kotlin showLineNumbers Adapty.logShowPaywall(paywall) ``` Request parameters: | Parameter | Presence | Description | | :---------- | :------- |:------------------------------------------------------------------------------------------------------------| | **paywall** | required | An [`AdaptyPaywall`](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall/) object. | --- # File: android-making-purchases --- --- title: "Make purchases in mobile app in Android SDK" description: "Guide on handling in-app purchases and subscriptions using Adapty." --- Displaying paywalls within your mobile app is an essential step in offering users access to premium content or services. However, simply presenting these paywalls is enough to support purchases only if you use [Paywall Builder](adapty-paywall-builder) to customize your paywalls. If you don't use the Paywall Builder, you must use a separate method called `.makePurchase()` to complete a purchase and unlock the desired content. This method serves as the gateway for users to engage with the paywalls and proceed with their desired transactions. If your paywall has an active promotional offer for the product a user is trying to buy, Adapty will automatically apply it at the time of purchase. :::warning Keep in mind that the introductory offer will be applied automatically only if you use the paywalls set up using the Paywall Builder. In other cases, you'll need to [verify the user's eligibility for an introductory offer on iOS](fetch-paywalls-and-products-android#check-intro-offer-eligibility-on-ios). Skipping this step may result in your app being rejected during release. Moreover, it could lead to charging the full price to users who are eligible for an introductory offer. ::: Make sure you've [done the initial configuration](quickstart) without skipping a single step. Without it, we can't validate purchases. ## Make purchase :::note **Using [Paywall Builder](adapty-paywall-builder)?** Purchases are processed automatically—you can skip this step. **Looking for step-by-step guidance?** Check out the [quickstart guide](android-implement-paywalls-manually) for end-to-end implementation instructions with full context. ::: ```kotlin showLineNumbers Adapty.makePurchase(activity, product, null) { result -> when (result) { is AdaptyResult.Success -> { when (val purchaseResult = result.value) { is AdaptyPurchaseResult.Success -> { val profile = purchaseResult.profile if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive == true) { // Grant access to the paid features } } is AdaptyPurchaseResult.UserCanceled -> { // Handle the case where the user canceled the purchase } is AdaptyPurchaseResult.Pending -> { // Handle deferred purchases (e.g., the user will pay offline with cash) } } } is AdaptyResult.Error -> { val error = result.error // Handle the error } } } ``` ```java showLineNumbers Adapty.makePurchase(activity, product, null, result -> { if (result instanceof AdaptyResult.Success) { AdaptyPurchaseResult purchaseResult = ((AdaptyResult.Success) result).getValue(); if (purchaseResult instanceof AdaptyPurchaseResult.Success) { AdaptyProfile profile = ((AdaptyPurchaseResult.Success) purchaseResult).getProfile(); AdaptyProfile.AccessLevel premium = profile.getAccessLevels().get("YOUR_ACCESS_LEVEL"); if (premium != null && premium.isActive()) { // Grant access to the paid features } } else if (purchaseResult instanceof AdaptyPurchaseResult.UserCanceled) { // Handle the case where the user canceled the purchase } else if (purchaseResult instanceof AdaptyPurchaseResult.Pending) { // Handle deferred purchases (e.g., the user will pay offline with cash) } } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // Handle the error } }); ``` Request parameters: | Parameter | Presence | Description | | :---------- | :------- | :-------------------------------------------------------------------------------------------------- | | **Product** | required | An [`AdaptyPaywallProduct`](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall-product/) object retrieved from the paywall. | Response parameters: | Parameter | Description | |---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Profile** |

If the request has been successful, the response contains this object. An [AdaptyProfile](https://android.adapty.io/adapty/com.adapty.models/-adapty-profile/) object provides comprehensive information about a user's access levels, subscriptions, and non-subscription purchases within the app.

Check the access level status to ascertain whether the user has the required access to the app.

| :::warning **Note:** if you're still on Apple's StoreKit version lower than v2.0 and Adapty SDK version lowers than v.2.9.0, you need to provide [Apple App Store shared secret](app-store-connection-configuration#step-4-enter-app-store-shared-secret) instead. This method is currently deprecated by Apple. ::: ## Change subscription when making a purchase When a user opts for a new subscription instead of renewing the current one, the way it works depends on the app store. For Google Play, the subscription isn't automatically updated. You'll need to manage the switch in your mobile app code as described below. To replace the subscription with another one in Android, call `.makePurchase()` method with the additional parameter: ```kotlin showLineNumbers Adapty.makePurchase( activity, product, AdaptyPurchaseParameters.Builder() .withSubscriptionUpdateParams(subscriptionUpdateParams) .build() ) { result -> when (result) { is AdaptyResult.Success -> { when (val purchaseResult = result.value) { is AdaptyPurchaseResult.Success -> { val profile = purchaseResult.profile // successful cross-grade } is AdaptyPurchaseResult.UserCanceled -> { // user canceled the purchase flow } is AdaptyPurchaseResult.Pending -> { // the purchase has not been finished yet, e.g. user will pay offline by cash } } } is AdaptyResult.Error -> { val error = result.error // Handle the error } } } ``` Additional request parameter: | Parameter | Presence | Description | | :--------------------------- | :------- | :----------------------------------------------------------- | | **subscriptionUpdateParams** | required | an [`AdaptySubscriptionUpdateParameters`](https://android.adapty.io/adapty/com.adapty.models/-adapty-subscription-update-parameters/) object. | ```java showLineNumbers Adapty.makePurchase( activity, product, new AdaptyPurchaseParameters.Builder() .withSubscriptionUpdateParams(subscriptionUpdateParams) .build(), result -> { if (result instanceof AdaptyResult.Success) { AdaptyPurchaseResult purchaseResult = ((AdaptyResult.Success) result).getValue(); if (purchaseResult instanceof AdaptyPurchaseResult.Success) { AdaptyProfile profile = ((AdaptyPurchaseResult.Success) purchaseResult).getProfile(); // successful cross-grade } else if (purchaseResult instanceof AdaptyPurchaseResult.UserCanceled) { // user canceled the purchase flow } else if (purchaseResult instanceof AdaptyPurchaseResult.Pending) { // the purchase has not been finished yet, e.g. user will pay offline by cash } } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // Handle the error } }); ``` Additional request parameter: | Parameter | Presence | Description | | :--------------------------- | :------- | :----------------------------------------------------------- | | **subscriptionUpdateParams** | required | an [`AdaptySubscriptionUpdateParameters`](https://android.adapty.io/adapty/com.adapty.models/-adapty-subscription-update-parameters/) object. | You can read more about subscriptions and replacement modes in the Google Developer documentation: - [About replacement modes](https://developer.android.com/google/play/billing/subscriptions#replacement-modes) - [Recommendations from Google for replacement modes](https://developer.android.com/google/play/billing/subscriptions#replacement-recommendations) - Replacement mode [`CHARGE_PRORATED_PRICE`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#CHARGE_PRORATED_PRICE()). Note: this method is available only for subscription upgrades. Downgrades are not supported. - Replacement mode [`DEFERRED`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#DEFERRED()). Note: A real subscription change will occur only when the current subscription billing period ends. ### Manage prepaid plans If your app users can purchase [prepaid plans](https://developer.android.com/google/play/billing/subscriptions#prepaid-plans) (e.g., buy a non-renewable subscription for several months), you can enable [pending transactions](https://developer.android.com/google/play/billing/subscriptions#pending) for prepaid plans. ```kotlin showLineNumbers AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withEnablePendingPrepaidPlans(true) .build() ``` ```java showLineNumbers new AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withEnablePendingPrepaidPlans(true) .build(); ``` --- # File: android-restore-purchase --- --- title: "Restore purchases in mobile app in Android SDK" description: "Learn how to restore purchases in Adapty to ensure seamless user experience." --- Restoring Purchases is a feature that allows users to regain access to previously purchased content, such as subscriptions or in-app purchases, without being charged again. This feature is especially useful for users who may have uninstalled and reinstalled the app or switched to a new device and want to access their previously purchased content without paying again. :::note In paywalls built with [Paywall Builder](adapty-paywall-builder), purchases are restored automatically without additional code from you. If that's your case — you can skip this step. ::: To restore a purchase if you do not use the [Paywall Builder](adapty-paywall-builder) to customize the paywall, call `.restorePurchases()` method: ```kotlin showLineNumbers Adapty.restorePurchases { result -> when (result) { is AdaptyResult.Success -> { val profile = result.value if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive == true) { // successful access restore } } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers Adapty.restorePurchases(result -> { if (result instanceof AdaptyResult.Success) { AdaptyProfile profile = ((AdaptyResult.Success) result).getValue(); if (profile != null) { AdaptyProfile.AccessLevel premium = profile.getAccessLevels().get("YOUR_ACCESS_LEVEL"); if (premium != null && premium.isActive()) { // successful access restore } } } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` Response parameters: | Parameter | Description | |---------|-----------| | **Profile** |

An [`AdaptyProfile`](https://android.adapty.io/adapty/com.adapty.models/-adapty-profile/) object. This model contains info about access levels, subscriptions, and non-subscription purchases.

Сheck the **access level status** to determine whether the user has access to the app.

| :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: --- # File: implement-observer-mode-android --- --- title: "Implement Observer mode in Android SDK" description: "Implement observer mode in Adapty to track user subscription events in Android SDK." --- If you already have your own purchase infrastructure and aren't ready to fully switch to Adapty, you can explore [Observer mode](observer-vs-full-mode). In its basic form, Observer Mode offers advanced analytics and seamless integration with attribution and analytics systems. If this meets your needs, you only need to: 1. Turn it on when configuring the Adapty SDK by setting the `observerMode` parameter to `true`. Follow the setup instructions for [Android](sdk-installation-android#configure-adapty-sdk). 2. [Report transactions](report-transactions-observer-mode-android) from your existing purchase infrastructure to Adapty. ## Observer mode setup Turn on the Observer mode if you handle purchases and subscription status yourself and use Adapty for sending subscription events and analytics. :::important When running in Observer mode, Adapty SDK won't close any transactions, so make sure you're handling it. ::: ```kotlin showLineNumbers class MyApplication : Application() { override fun onCreate() { super.onCreate() Adapty.activate( applicationContext, AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withObserverMode(true) //default false .build() ) } ``` ```java showLineNumbers public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); Adapty.activate( applicationContext, new AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withObserverMode(true) //default false .build() ); } ``` Parameters: | Parameter | Description | | --------------------------- | ------------------------------------------------------------ | | observerMode | A boolean value that controls [Observer mode](observer-vs-full-mode). The default value is `false`. | ## Using Adapty paywalls in Observer Mode If you also want to use Adapty's paywalls and A/B testing features, you can — but it requires some extra setup in Observer mode. Here's what you'll need to do in addition to the steps above: 1. Display paywalls as usual for [remote config paywalls](present-remote-config-paywalls-android.md). For Paywall Builder paywalls, follow the specific setup guides for [Android](android-present-paywall-builder-paywalls-in-observer-mode). 3. [Associate paywalls](report-transactions-observer-mode-android) with purchase transactions. --- # File: report-transactions-observer-mode-android --- --- title: "Report transactions in Observer Mode in Android SDK" description: "Report purchase transactions in Adapty Observer Mode for user insights and revenue tracking in Android SDK." --- In Observer mode, the Adapty SDK can't track purchases made through your existing purchase system on its own. You need to report transactions from your app store. It's crucial to set this up **before** releasing your app to avoid errors in analytics. Use `reportTransaction` to explicitly report each transaction for Adapty to recognize it. :::warning **Don't skip transaction reporting!** If you don't call `reportTransaction`, Adapty won't recognize the transaction, it won't appear in analytics, and it won't be sent to integrations. ::: If you use Adapty paywalls, include the `variationId` when reporting a transaction. This links the purchase to the paywall that triggered it, ensuring accurate paywall analytics. ```kotlin showLineNumbers val transactionInfo = TransactionInfo.fromPurchase(purchase) Adapty.reportTransaction(transactionInfo, variationId) { result -> if (result is AdaptyResult.Success) { // success } } ``` Parameters: | Parameter | Presence | Description | | --------------- | -------- | ------------------------------------------------------------ | | transactionInfo | required | The TransactionInfo from the purchase, where the purchase is an instance of the billing library [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) class. | | variationId | optional | The string identifier of the variation. You can get it using `variationId` property of the [AdaptyPaywall](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall/) object. | ```java showLineNumbers TransactionInfo transactionInfo = TransactionInfo.fromPurchase(purchase); Adapty.reportTransaction(transactionInfo, variationId, result -> { if (result instanceof AdaptyResult.Success) { // success } }); ``` Parameters: | Parameter | Presence | Description | | --------------- | -------- | ------------------------------------------------------------ | | transactionInfo | required | The TransactionInfo from the purchase, where the purchase is an instance of the billing library [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) class. | | variationId | optional | The string identifier of the variation. You can get it using `variationId` property of the [AdaptyPaywall](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall/) object. | In Observer mode, the Adapty SDK can't track purchases made through your existing purchase system on its own. You need to report transactions from your app store or restore them. It's crucial to set this up **before** releasing your app to avoid errors in analytics. Use `restorePurchases` to report the transaction to Adapty. :::warning **Don't skip purchase restoring!** If you don't call `restorePurchases`, Adapty won't recognize the transaction, it won't appear in analytics, and it won't be sent to integrations. ::: If you use Adapty paywalls, link your transaction to the paywall that led to the purchase using the `setVariationId` method. This ensures the purchase is correctly attributed to the triggering paywall for accurate analytics. This step is only necessary if you're using Adapty paywalls. ```kotlin showLineNumbers Adapty.restorePurchases { result -> if (result is AdaptyResult.Success) { // success } } Adapty.setVariationId(transactionId, variationId) { error -> if (error == null) { // success } } ``` Parameters: | Parameter | Presence | Description | | ------------- | -------- | ------------------------------------------------------------ | | transactionId | required | String identifier (`purchase.getOrderId`) of the purchase, where the purchase is an instance of the billing library [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) class. | | variationId | required | The string identifier of the variation. You can get it using `variationId` property of the [AdaptyPaywall](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall/) object. | ```java showLineNumbers Adapty.restorePurchases(result -> { if (result instanceof AdaptyResult.Success) { // success } }); Adapty.setVariationId(transactionId, variationId, error -> { if (error == null) { // success } }); ``` Parameters: | Parameter | Presence | Description | | ------------- | -------- | ------------------------------------------------------------ | | transactionId | required | String identifier (`purchase.getOrderId`) of the purchase, where the purchase is an instance of the billing library [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) class. | | variationId | required | The string identifier of the variation. You can get it using `variationId` property of the [AdaptyPaywall](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall/) object. | **Reporting transactions** Use `restorePurchases` to report a transaction to Adapty in Observer Mode, as explained on the [Restore Purchases in Mobile Code](android-restore-purchase) page. :::warning **Don't skip transaction reporting!** If you don't call `restorePurchases`, Adapty won't recognize the transaction, it won't appear in analytics, and it won't be sent to integrations. ::: **Associating paywalls to transactions** Adapty SDK cannot determine the source of purchases, as you are the one processing them. Therefore, if you intend to use paywalls and/or A/B tests in Observer mode, you need to associate the transaction coming from your app store with the corresponding paywall in your mobile app code. This is important to get right before releasing your app, otherwise, it will lead to errors in analytics. ```kotlin Adapty.setVariationId(transactionId, variationId) { error -> if (error == null) { // success } } ``` Request parameters: | Parameter | Presence | Description | | ------------- | -------- | ------------------------------------------------------------ | | transactionId | required | String identifier (purchase.getOrderId of the purchase, where the purchase is an instance of the billing library [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) class. | | variationId | required | The string identifier of the variation. You can get it using `variationId` property of the [AdaptyPaywall](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall/) object. | ```java Adapty.setVariationId(transactionId, variationId, error -> { if (error == null) { // success } }); ``` | Parameter | Presence | Description | | ------------------------------------------------- | -------- | ------------------------------------------------------------ | | transactionId | required | String identifier (purchase.getOrderId of the purchase, where the purchase is an instance of the billing library [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) class. | | variationId | required | The string identifier of the variation. You can get it using `variationId` property of the [AdaptyPaywall](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall/) object. | --- # File: android-present-paywall-builder-paywalls-in-observer-mode --- --- title: "Present Paywall Builder paywalls in Observer mode in Android SDK" description: "Learn how to present paywalls in observer mode using Adapty’s Paywall Builder." --- If you've customized a paywall using the Paywall Builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. :::warning This section refers to [Observer mode](observer-vs-full-mode) only. If you do not work in the Observer mode, refer to the [Android - Present Paywall Builder paywalls](android-present-paywalls) topic instead. :::
Before you start presenting paywalls (Click to Expand) 1. Set up initial integration of Adapty [with the Google Play](initial-android) and [with the App Store](initial_ios). 2. Install and configure Adapty SDK. Make sure to set the `observerMode` parameter to `true`. Refer to our framework-specific instructions [for Android](sdk-installation-android). 3. [Create products](create-product) in the Adapty Dashboard. 4. [Configure paywalls, assign products to them](create-paywall), and customize them using Paywall Builder in the Adapty Dashboard. 5. [Create placements and assign your paywalls to them](create-placement) in the Adapty Dashboard. 6. [Fetch Paywall Builder paywalls and their configuration](android-get-pb-paywalls) in your mobile app code.

1. Implement the `AdaptyUiObserverModeHandler`. The `onPurchaseInitiated` event will inform you that the user has initiated a purchase. You can trigger your custom purchase flow in response to this callback: ```kotlin showLineNumbers val observerModeHandler = AdaptyUiObserverModeHandler { product, paywall, paywallView, onStartPurchase, onFinishPurchase -> onStartPurchase() yourBillingClient.makePurchase( product, onSuccess = { purchase -> onFinishPurchase() //handle success }, onError = { onFinishPurchase() //handle error }, onCancel = { onFinishPurchase() //handle cancel } ) } ``` ```java showLineNumbers AdaptyUiObserverModeHandler observerModeHandler = (product, paywall, paywallView, onStartPurchase, onFinishPurchase) -> { onStartPurchase.invoke(); yourBillingClient.makePurchase( product, purchase -> { onFinishPurchase.invoke(); //handle success }, error -> { onFinishPurchase.invoke(); //handle error }, () -> { //cancellation onFinishPurchase.invoke(); //handle cancel } ); }; ``` To handle restores in Observer mode, override `getRestoreHandler()`. By default it returns `null`, which uses Adapty's built-in `Adapty.restorePurchases()` flow. To provide your own restore implementation: ```kotlin showLineNumbers val observerModeHandler = object : AdaptyUiObserverModeHandler { // onPurchaseInitiated implementation (see above) override fun getRestoreHandler() = AdaptyUiObserverModeHandler.RestoreHandler { onStartRestore, onFinishRestore -> onStartRestore() yourBillingClient.restorePurchases( onSuccess = { restoredPurchases -> onFinishRestore() //handle successful restore }, onError = { onFinishRestore() //handle error } ) } } ``` ```java showLineNumbers AdaptyUiObserverModeHandler observerModeHandler = new AdaptyUiObserverModeHandler() { // onPurchaseInitiated implementation (see above) @Override public RestoreHandler getRestoreHandler() { return (onStartRestore, onFinishRestore) -> { onStartRestore.invoke(); yourBillingClient.restorePurchases( restoredPurchases -> { onFinishRestore.invoke(); //handle successful restore }, error -> { onFinishRestore.invoke(); //handle error } ); }; } }; ``` Remember to invoke the following callbacks to notify AdaptyUI about the purchase or restore process. This is necessary for proper paywall behavior, such as showing the loader: | Callback | Description | | :----------------- |:---------------------------------------------------------------------------------------| | onStartPurchase() | The callback should be invoked to notify AdaptyUI that the purchase is started. | | onFinishPurchase() | The callback should be invoked to notify AdaptyUI that the purchase is finished. | | onStartRestore() | Optional. The callback can be invoked to notify AdaptyUI that the restore is started. | | onFinishRestore() | Optional. The callback can be invoked to notify AdaptyUI that the restore is finished. | 2. In order to display the visual paywall on the device screen, you must first configure it. To do this, call the method `AdaptyUI.getPaywallView()` or create the `AdaptyPaywallView` directly: ```kotlin showLineNumbers val paywallView = AdaptyUI.getPaywallView( activity, viewConfiguration, products, eventListener, personalizedOfferResolver, tagResolver, timerResolver, observerModeHandler, ) ``` ```kotlin showLineNumbers val paywallView = AdaptyPaywallView(activity) // or retrieve it from xml ... with(paywallView) { showPaywall( viewConfiguration, products, eventListener, personalizedOfferResolver, tagResolver, timerResolver, observerModeHandler, ) } ``` ```java showLineNumbers AdaptyPaywallView paywallView = AdaptyUI.getPaywallView( activity, viewConfiguration, products, eventListener, personalizedOfferResolver, tagResolver, timerResolver, observerModeHandler ); ``` ```java showLineNumbers AdaptyPaywallView paywallView = new AdaptyPaywallView(activity); //add to the view hierarchy if needed, or you receive it from xml ... paywallView.showPaywall(viewConfiguration, products, eventListener, personalizedOfferResolver, tagResolver, timerResolver, observerModeHandler); ``` ```xml showLineNumbers ``` After the view has been successfully created, you can add it to the view hierarchy and display it. To do this, use this composable function: ```kotlin showLineNumbers AdaptyPaywallScreen( viewConfiguration, products, eventListener, personalizedOfferResolver, tagResolver, timerResolver, ) ``` Request parameters: | Parameter | Presence | Description | |---------|--------|-----------| | **Products** | optional | Provide an array of `AdaptyPaywallProduct `to optimize the display timing of products on the screen. If `null` is passed, AdaptyUI will automatically fetch the required products. | | **ViewConfiguration** | required | Supply an `AdaptyViewConfiguration` object containing visual details of the paywall. Use the `Adapty.getViewConfiguration(paywall)` method to load it. Refer to [Fetch the visual configuration of paywall](#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) topic for more details. | | **EventListener** | optional | Provide an `AdaptyUiEventListener` to observe paywall events. Extending AdaptyUiDefaultEventListener is recommended for ease of use. Refer to [Handling paywall events](android-handling-events) topic for more details. | | **PersonalizedOfferResolver** | optional | To indicate personalized pricing ([read more](https://developer.android.com/google/play/billing/integrate#personalized-price) ), implement `AdaptyUiPersonalizedOfferResolver` and pass your own logic that maps `AdaptyPaywallProduct` to true if the product's price is personalized, otherwise false. | | **TagResolver** | optional | Use `AdaptyUiTagResolver` to resolve custom tags within the paywall text. This resolver takes a tag parameter and resolves it to a corresponding string. Refer to [Custom tags in Paywall Builder](custom-tags-in-paywall-builder) topic for more details. | | **ObserverModeHandler** | required for Observer mode | The `AdaptyUiObserverModeHandler` you've implemented in the previous step. | | **variationId** | required | The string identifier of the variation. You can get it using `variationId` property of the [`AdaptyPaywall`](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall/) object. | | **transaction** | required |

For iOS, StoreKit1: an [`SKPaymentTransaction`](https://developer.apple.com/documentation/storekit/skpaymenttransaction) object.

For iOS, StoreKit 2: [Transaction](https://developer.apple.com/documentation/storekit/transaction) object.

For Android: String identifier (`purchase.getOrderId()`) of the purchase, where the purchase is an instance of the billing library [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) class.

|
Before you start presenting paywalls (Click to Expand) 1. Set up initial integration of Adapty [with the Google Play](initial-android) and [with the App Store](initial_ios). 2. Install and configure Adapty SDK. Make sure to set the `observerMode` parameter to `true`. Refer to our framework-specific instructions [for Android](sdk-installation-android), [React Native](sdk-installation-reactnative#configure-adapty-sdks), [Flutter](sdk-installation-flutter#configure-adapty-sdk), and [Unity](sdk-installation-unity#configure-adapty-sdk). 3. [Create products](create-product) in the Adapty Dashboard. 4. [Configure paywalls, assign products to them](create-paywall), and customize them using Paywall Builder in the Adapty Dashboard. 5. [Create placements and assign your paywalls to them](create-placement) in the Adapty Dashboard. 6. [Fetch Paywall Builder paywalls and their configuration](android-get-pb-paywalls) in your mobile app code.
1. Implement the `AdaptyUiObserverModeHandler`. The `AdaptyUiObserverModeHandler`'s callback (`onPurchaseInitiated`) informs you when the user initiates a purchase. You can trigger your custom purchase flow in response to this callback like this: ```kotlin showLineNumbers val observerModeHandler = AdaptyUiObserverModeHandler { product, paywall, paywallView, onStartPurchase, onFinishPurchase -> onStartPurchase() yourBillingClient.makePurchase( product, onSuccess = { purchase -> onFinishPurchase() //handle success }, onError = { onFinishPurchase() //handle error }, onCancel = { onFinishPurchase() //handle cancel } ) } ``` ```java showLineNumbers AdaptyUiObserverModeHandler observerModeHandler = (product, paywall, paywallView, onStartPurchase, onFinishPurchase) -> { onStartPurchase.invoke(); yourBillingClient.makePurchase( product, purchase -> { onFinishPurchase.invoke(); //handle success }, error -> { onFinishPurchase.invoke(); //handle error }, () -> { //cancellation onFinishPurchase.invoke(); //handle cancel } ); }; ``` Also, remember to invoke these callbacks to AdaptyUI. This is necessary for proper paywall behavior, such as showing the loader, among other things: | Callback in Kotlin | Callback in Java | Description | | :----------------- | :------------------------ | :-------------------------------------------------------------------------------------------------------------------------------- | | onStartPurchase() | onStartPurchase.invoke() | The callback should be invoked to notify AdaptyUI that the purchase is started. | | onFinishPurchase() | onFinishPurchase.invoke() | The callback should be invoked to notify AdaptyUI that the purchase is finished successfully or not, or the purchase is canceled. | 2. In order to display the visual paywall, you must first initialize it. To do this, call the method `AdaptyUI.getPaywallView()` or create the `AdaptyPaywallView` directly: ```kotlin showLineNumbers val paywallView = AdaptyUI.getPaywallView( activity, viewConfiguration, products, AdaptyPaywallInsets.of(topInset, bottomInset), eventListener, personalizedOfferResolver, tagResolver, observerModeHandler, ) //======= OR ======= val paywallView = AdaptyPaywallView(activity) // or retrieve it from xml ... with(paywallView) { setEventListener(eventListener) setObserverModeHandler(observerModeHandler) showPaywall( viewConfiguration, products, AdaptyPaywallInsets.of(topInset, bottomInset), personalizedOfferResolver, tagResolver, ) } ``` ```java showLineNumbers AdaptyPaywallView paywallView = AdaptyUI.getPaywallView( activity, viewConfiguration, products, AdaptyPaywallInsets.of(topInset, bottomInset), eventListener, personalizedOfferResolver, tagResolver, observerModeHandler ); //======= OR ======= AdaptyPaywallView paywallView = new AdaptyPaywallView(activity); //add to the view hierarchy if needed, or you receive it from xml ... paywallView.setEventListener(eventListener); paywallView.setObserverModeHandler(observerModeHandler); paywallView.showPaywall(viewConfiguration, products, AdaptyPaywallInsets.of(topInset, bottomInset), personalizedOfferResolver); ``` ```xml showLineNumbers ``` After the view has been successfully created, you can add it to the view hierarchy and display it. Request parameters: | Parameter | Presence | Description | |---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Products** | optional | Provide an array of `AdaptyPaywallProduct `to optimize the display timing of products on the screen. If `null` is passed, AdaptyUI will automatically fetch the required products. | | **ViewConfiguration** | required | Supply an `AdaptyViewConfiguration` object containing visual details of the paywall. Use the `Adapty.getViewConfiguration(paywall)` method to load it. Refer to [Fetch the visual configuration of paywall](android-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) topic for more details. | | **Insets** | required | Define an `AdaptyPaywallInsets` object containing information about the area overlapped by system bars, creating vertical margins for content. If neither the status bar nor the navigation bar overlaps the `AdaptyPaywallView`, pass `AdaptyPaywallInsets.NONE`. For fullscreen mode where system bars overlap part of your UI, obtain insets as shown under the table. | | **EventListener** | optional | Provide an `AdaptyUiEventListener` to observe paywall events. Extending AdaptyUiDefaultEventListener is recommended for ease of use. Refer to [Handling paywall events](android-handling-events) topic for more details. | | **PersonalizedOfferResolver** | optional | To indicate personalized pricing ([read more](https://developer.android.com/google/play/billing/integrate#personalized-price) ), implement `AdaptyUiPersonalizedOfferResolver` and pass your own logic that maps `AdaptyPaywallProduct` to true if the product's price is personalized, otherwise false. | | **TagResolver** | optional | Use `AdaptyUiTagResolver` to resolve custom tags within the paywall text. This resolver takes a tag parameter and resolves it to a corresponding string. Refer to [Custom tags in Paywall Builder](custom-tags-in-paywall-builder) topic for more details. | | **ObserverModeHandler** | required for Observer mode | The `AdaptyUiObserverModeHandler` you've implemented in the previous step. | | **variationId** | required | The string identifier of the variation. You can get it using `variationId` property of the [`AdaptyPaywall`](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall/) object. | | **transaction** | required |

For iOS, StoreKit1: an [`SKPaymentTransaction`](https://developer.apple.com/documentation/storekit/skpaymenttransaction) object.

For iOS, StoreKit 2: [Transaction](https://developer.apple.com/documentation/storekit/transaction) object.

For Android: String identifier (`purchase.getOrderId()`) of the purchase, where the purchase is an instance of the billing library [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) class.

| For fullscreen mode where system bars overlap part of your UI, obtain insets in the following way: ```kotlin showLineNumbers import androidx.core.graphics.Insets import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat //create extension function fun View.onReceiveSystemBarsInsets(action: (insets: Insets) -> Unit) { ViewCompat.setOnApplyWindowInsetsListener(this) { _, insets -> val systemBarInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) ViewCompat.setOnApplyWindowInsetsListener(this, null) action(systemBarInsets) insets } } //and then use it with the view paywallView.onReceiveSystemBarsInsets { insets -> val paywallInsets = AdaptyPaywallInsets.of(insets.top, insets.bottom) paywallView.setEventListener(eventListener) paywallView.setObserverModeHandler(observerModeHandler) paywallView.showPaywall(viewConfig, products, paywallInsets, personalizedOfferResolver, tagResolver) } ``` ```java showLineNumbers import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; ... ViewCompat.setOnApplyWindowInsetsListener(paywallView, (view, insets) -> { Insets systemBarInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()); ViewCompat.setOnApplyWindowInsetsListener(paywallView, null); AdaptyPaywallInsets paywallInsets = AdaptyPaywallInsets.of(systemBarInsets.top, systemBarInsets.bottom); paywallView.setEventListener(eventListener); paywallView.setObserverModeHandler(observerModeHandler); paywallView.showPaywall(viewConfiguration, products, paywallInsets, personalizedOfferResolver, tagResolver); return insets; }); ``` Returns: | Object | Description | | :------------------ | :------------------------------------------------- | | `AdaptyPaywallView` | object, representing the requested paywall screen. | :::warning Don't forget to [Associate paywalls to purchase transactions](report-transactions-observer-mode-android.md). Otherwise, Adapty will not determine the source paywall of the purchase. :::
--- # File: android-troubleshoot-purchases --- --- title: "Troubleshoot purchases in Android SDK" description: "Troubleshoot purchases in Android SDK" --- This guide helps you resolve common issues when implementing purchases manually in the Android SDK. ## makePurchase is called successfully, but the profile is not being updated **Issue**: The `makePurchase` method completes successfully, but the user's profile and subscription status are not updated in Adapty. **Reason**: This usually indicates incomplete Google Play Store setup or configuration issues. **Solution**: Ensure you've completed all the [Google Play setup steps](https://adapty.io/docs/initial-android). ## makePurchase is invoked twice **Issue**: The `makePurchase` method is being called multiple times for the same purchase. **Reason**: This typically happens when the purchase flow is triggered multiple times due to UI state management issues or rapid user interactions. **Solution**: Ensure you've completed all the [Google Play setup steps](https://adapty.io/docs/initial-android). ## AdaptyError.cantMakePayments in observer mode **Issue**: You're getting `AdaptyError.cantMakePayments` when using `makePurchase` in observer mode. **Reason**: In observer mode, you should handle purchases on your side, not use Adapty's `makePurchase` method. **Solution**: If you use `makePurchase` for purchases, turn off the observer mode. You need either to use `makePurchase` or handle purchases on your side in the observer mode. See [Implement Observer mode](implement-observer-mode-android) for more details. ## Adapty error: (code: 103, message: Play Market request failed on purchases updated: responseCode=3, debugMessage=Billing Unavailable, detail: null) **Issue**: You're receiving a billing unavailable error from Google Play Store. **Reason**: This error is not related to Adapty. It's a Google Play Billing Library error indicating that billing is not available on the device. **Solution**: This error is not related to Adapty. You can check and find out more about it in the Play Store documentation: [Handle BillingResult response codes](https://developer.android.com/google/play/billing/errors#billing_unavailable_error_code_3) | Play Billing | Android Developers. ## Not found makePurchasesCompletionHandlers **Issue**: You're encountering issues with `makePurchasesCompletionHandlers` not being found. **Reason**: This is typically related to sandbox testing issues. **Solution**: Create a new sandbox user and try again. This often resolves sandbox-related purchase completion handler issues. ## Other issues **Issue**: You're experiencing other purchase-related problems not covered above. **Solution**: Migrate the SDK to the latest version using the [migration guides](android-sdk-migration-guides) if needed. Many issues are resolved in newer SDK versions. --- # File: android-identifying-users --- --- title: "Identify users in Android SDK" description: "Identify users in Adapty to improve personalized subscription experiences (Android)." --- Adapty creates an internal profile ID for every user. However, if you have your own authentication system, you should set your own Customer User ID. You can find users by their Customer User ID in the [Profiles](profiles-crm) section and use it in the [server-side API](getting-started-with-server-side-api), which will be sent to all integrations. ### Setting customer user ID on configuration If you have a user ID during configuration, just pass it as `customerUserId` parameter to `.activate()` method: ```kotlin showLineNumbers Adapty.activate(applicationContext, "PUBLIC_SDK_KEY", customerUserId = "YOUR_USER_ID") ``` :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ### Setting customer user ID after configuration If you don't have a user ID in the SDK configuration, you can set it later at any time with the `.identify()` method. The most common cases for using this method are after registration or authorization, when the user switches from being an anonymous user to an authenticated user. ```kotlin showLineNumbers Adapty.identify("YOUR_USER_ID") { error -> if (error == null) { // successful identify } } ``` ```java showLineNumbers Adapty.identify("YOUR_USER_ID", error -> { if (error == null) { // successful identify } }); ``` Request parameters: - **Customer User ID** (required): a string user identifier. :::warning Resubmitting of significant user data In some cases, such as when a user logs into their account again, Adapty's servers already have information about that user. In these scenarios, the Adapty SDK will automatically switch to work with the new user. If you passed any data to the anonymous user, such as custom attributes or attributions from third-party networks, you should resubmit that data for the identified user. It's also important to note that you should re-request all paywalls and products after identifying the user, as the new user's data may be different. ::: ### Logging out and logging in You can logout the user anytime by calling `.logout()` method: ```kotlin showLineNumbers Adapty.logout { error -> if (error == null) { // successful logout } } ``` ```java showLineNumbers Adapty.logout(error -> { if (error == null) { // successful logout } }); ``` You can then login the user using `.identify()` method. --- # File: android-setting-user-attributes --- --- title: "Set user attributes in Android SDK" description: "Learn how to set user attributes in Adapty to enable better audience segmentation." --- You can set optional attributes such as email, phone number, etc, to the user of your app. You can then use attributes to create user [segments](segments) or just view them in CRM. ### Setting user attributes To set user attributes, call `.updateProfile()` method: ```kotlin showLineNumbers val builder = AdaptyProfileParameters.Builder() .withEmail("email@email.com") .withPhoneNumber("+18888888888") .withFirstName("John") .withLastName("Appleseed") .withGender(AdaptyProfile.Gender.OTHER) .withBirthday(AdaptyProfile.Date(1970, 1, 3)) Adapty.updateProfile(builder.build()) { error -> if (error != null) { // handle the error } } ``` ```java showLineNumbers AdaptyProfileParameters.Builder builder = new AdaptyProfileParameters.Builder() .withEmail("email@email.com") .withPhoneNumber("+18888888888") .withFirstName("John") .withLastName("Appleseed") .withGender(AdaptyProfile.Gender.OTHER) .withBirthday(new AdaptyProfile.Date(1970, 1, 3)); Adapty.updateProfile(builder.build(), error -> { if (error != null) { // handle the error } }); ``` Please note that the attributes that you've previously set with the `updateProfile` method won't be reset. :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ### The allowed keys list The allowed keys `` of `AdaptyProfileParameters.Builder` and the values `` are listed below: | Key | Value | |---|-----| |

email

phoneNumber

firstName

lastName

| String | | gender | Enum, allowed values are: `female`, `male`, `other` | | birthday | Date | ### Custom user attributes You can set your own custom attributes. These are usually related to your app usage. For example, for fitness applications, they might be the number of exercises per week, for language learning app user's knowledge level, and so on. You can use them in segments to create targeted paywalls and offers, and you can also use them in analytics to figure out which product metrics affect the revenue most. ```kotlin showLineNumbers builder.withCustomAttribute("key1", "value1") ``` ```java showLineNumbers builder.withCustomAttribute("key1", "value1"); ``` To remove existing key, use `.withRemoved(customAttributeForKey:)` method: ```kotlin showLineNumbers builder.withRemovedCustomAttribute("key2") ``` ```java showLineNumbers builder.withRemovedCustomAttribute("key2"); ``` Sometimes you need to figure out what custom attributes have already been installed before. To do this, use the `customAttributes` field of the `AdaptyProfile` object. :::warning Keep in mind that the value of `customAttributes` may be out of date since the user attributes can be sent from different devices at any time so the attributes on the server might have been changed after the last sync. ::: ### Limits - Up to 30 custom attributes per user - Key names are up to 30 characters long. The key name can include alphanumeric characters and any of the following: `_` `-` `.` - Value can be a string or float with no more than 50 characters. --- # File: android-listen-subscription-changes --- --- title: "Check subscription status in Android SDK" description: "Track and manage user subscription status in Adapty for improved customer retention in your Android app." --- With Adapty, keeping track of subscription status is made easy. You don't have to manually insert product IDs into your code. Instead, you can effortlessly confirm a user's subscription status by checking for an active [access level](access-level). Before you start checking subscription status, set up [Real-time Developer Notifications (RTDN)](enable-real-time-developer-notifications-rtdn). ## Access level and the AdaptyProfile object Access levels are properties of the [AdaptyProfile](https://android.adapty.io/adapty/com.adapty.models/-adapty-profile/) object. We recommend retrieving the profile when your app starts, such as when you [identify a user](android-identifying-users#setting-customer-user-id-on-configuration) , and then updating it whenever changes occur. This way, you can use the profile object without repeatedly requesting it. To be notified of profile updates, listen for profile changes as described in the [Listening for profile updates, including access levels](android-listen-subscription-changes.md) section below. :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ## Retrieving the access level from the server To get the access level from the server, use the `.getProfile()` method: ```kotlin showLineNumbers Adapty.getProfile { result -> when (result) { is AdaptyResult.Success -> { val profile = result.value // check the access } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers Adapty.getProfile(result -> { if (result instanceof AdaptyResult.Success) { AdaptyProfile profile = ((AdaptyResult.Success) result).getValue(); // check the access } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` Response parameters: | Parameter | Description | | --------- | ------------------------------------------------------------ | | Profile |

An [AdaptyProfile](https://android.adapty.io/adapty/com.adapty.models/-adapty-profile/) object. Generally, you have to check only the access level status of the profile to determine whether the user has premium access to the app.

The `.getProfile` method provides the most up-to-date result as it always tries to query the API. If for some reason (e.g. no internet connection), the Adapty SDK fails to retrieve information from the server, the data from the cache will be returned. It is also important to note that the Adapty SDK updates `AdaptyProfile` cache regularly, to keep this information as up-to-date as possible.

| The `.getProfile()` method provides you with the user profile from which you can get the access level status. You can have multiple access levels per app. For example, if you have a newspaper app and sell subscriptions to different topics independently, you can create access levels "sports" and "science". But most of the time, you will only need one access level, in that case, you can just use the default "premium" access level. Here is an example for checking for the default "premium" access level: ```kotlin showLineNumbers Adapty.getProfile { result -> when (result) { is AdaptyResult.Success -> { val profile = result.value if (profile.accessLevels["premium"]?.isActive == true) { // grant access to premium features } } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers Adapty.getProfile(result -> { if (result instanceof AdaptyResult.Success) { AdaptyProfile profile = ((AdaptyResult.Success) result).getValue(); AdaptyProfile.AccessLevel premium = profile.getAccessLevels().get("premium"); if (premium != null && premium.isActive()) { // grant access to premium features } } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` ### Listening for subscription status updates Whenever the user's subscription changes, Adapty fires an event. To receive messages from Adapty, you need to make some additional configuration: ```kotlin showLineNumbers Adapty.setOnProfileUpdatedListener { profile -> // handle any changes to subscription state } ``` ```java showLineNumbers t Adapty.setOnProfileUpdatedListener(profile -> { // handle any changes to subscription state }); ``` Adapty also fires an event at the start of the application. In this case, the cached subscription status will be passed. ### Subscription status cache The cache implemented in the Adapty SDK stores the subscription status of the profile. This means that even if the server is unavailable, the cached data can be accessed to provide information about the profile's subscription status. However, it's important to note that direct data requests from the cache are not possible. The SDK periodically queries the server every minute to check for any updates or changes related to the profile. If there are any modifications, such as new transactions or other updates, they will be sent to the cached data in order to keep it synchronized with the server. --- # File: kids-mode-android --- --- title: "Kids Mode in Android SDK" description: "Easily enable Kids Mode to comply with Google policies. No GAID or ad data collected in Android SDK." --- If your Android application is intended for kids, you must follow the policies of [Google](https://support.google.com/googleplay/android-developer/answer/9893335). If you're using the Adapty SDK, a few simple steps will help you configure it to meet these policies and pass app store reviews. ## What's required? You need to configure the Adapty SDK to disable the collection of: - [Android Advertising ID (AAID/GAID)](https://support.google.com/googleplay/android-developer/answer/6048248) - [IP address](https://www.ftc.gov/system/files/ftc_gov/pdf/p235402_coppa_application.pdf) In addition, we recommend using customer user ID carefully. User ID in format `` will be definitely treated as gathering personal data as well as using email. For Kids Mode, a best practice is to use randomized or anonymized identifiers (e.g., hashed IDs or device-generated UUIDs) to ensure compliance. ## Enabling Kids Mode ### Updates in the Adapty Dashboard In the Adapty Dashboard, you need to disable the IP address collection. To do this, go to [App settings](https://app.adapty.io/settings/general) and click **Disable IP address collection** under **Collect users' IP address**. ### Updates in your mobile app code To comply with policies, you need to disable the collection of the Android Advertising ID (AAID/GAID) and IP address when initializing the Adapty SDK: **Kotlin:** ```kotlin showLineNumbers override fun onCreate() { super.onCreate() Adapty.activate( applicationContext, AdaptyConfig.Builder("PUBLIC_SDK_KEY") // highlight-start .withAdIdCollectionDisabled(true) // set to `true` .withIpAddressCollectionDisabled(true) // set to `true` // highlight-end .build() ) } ``` **Java:** ```java showLineNumbers @Override public void onCreate() { super.onCreate(); Adapty.activate( applicationContext, new AdaptyConfig.Builder("PUBLIC_SDK_KEY") // highlight-start .withAdIdCollectionDisabled(true) // set to `true` .withIpAddressCollectionDisabled(true) // set to `true` // highlight-end .build() ); } ``` --- # File: android-get-onboardings --- --- title: "Get onboardings in Android SDK" description: "Learn how to retrieve onboardings in Adapty for Android." --- After [you designed the visual part for your onboarding](design-onboarding.md) with the builder in the Adapty Dashboard, you can display it in your Android app. The first step in this process is to get the onboarding associated with the placement and its view configuration as described below. Before you start, ensure that: 1. You have installed [Adapty Android SDK](sdk-installation-android.md) version 3.8.0 or higher. 2. You have [created an onboarding](create-onboarding.md). 3. You have added the onboarding to a [placement](placements.md). ## Fetch onboarding When you create an [onboarding](onboardings.md) with our no-code builder, it's stored as a container with configuration that your app needs to fetch and display. This container manages the entire experience - what content appears, how it's presented, and how user interactions (like quiz answers or form inputs) are processed. The container also automatically tracks analytics events, so you don't need to implement separate view tracking. For best performance, fetch the onboarding configuration early to give images enough time to download before showing to users. To get an onboarding, use the `getOnboarding` method: ```kotlin showLineNumbers Adapty.getOnboarding("YOUR_PLACEMENT_ID") { result -> when (result) { is AdaptyResult.Success -> { val onboarding = result.value // the requested onboarding } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` Parameters: | Parameter | Presence | Description | |---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the onboarding localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.

| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

Adapty SDK stores onboardings locally in two layers: regularly updated cache described above and fallback onboardings. We also use CDN to fetch onboardings faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your onboardings while ensuring reliability even in cases where internet connection is scarce.

| | **loadTimeout** | default: 5 sec |

This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.

Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.

For Android: You can create `TimeInterval` with extension functions (like `5.seconds`, where `.seconds` is from `import com.adapty.utils.seconds`), or `TimeInterval.seconds(5)`. To set no limitation, use `TimeInterval.INFINITE`.

| Response parameters: | Parameter | Description | |:----------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------| | Onboarding | An [`AdaptyOnboarding`](https://android.adapty.io/adapty/com.adapty.models/-adapty-onboarding/) object with: the onboarding identifier and configuration, remote config, and several other properties. | ## Speed up onboarding fetching with default audience onboarding Typically, onboardings are fetched almost instantly, so you don't need to worry about speeding up this process. However, in cases where you have numerous audiences and onboardings, and your users have a weak internet connection, fetching a onboarding may take longer than you'd like. In such situations, you might want to display a default onboarding to ensure a smooth user experience rather than showing no onboarding at all. To address this, you can use the `getOnboardingForDefaultAudience` method, which fetches the onboarding of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the onboarding by the `getOnboarding` method, as detailed in the [Fetch Onboarding](#fetch-onboarding) section above. :::warning Consider using `getOnboarding` instead of `getOnboardingForDefaultAudience`, as the latter has important limitations: - **Compatibility issues**: May create problems when supporting multiple app versions, requiring either backward-compatible designs or accepting that older versions might display incorrectly. - **No personalization**: Only shows content for the "All Users" audience, removing targeting based on country, attribution, or custom attributes. If faster fetching outweighs these drawbacks for your use case, use `getOnboardingForDefaultAudience` as shown below. Otherwise, use `getOnboarding` as described [above](#fetch-onboarding). ::: ```kotlin Adapty.getOnboardingForDefaultAudience("YOUR_PLACEMENT_ID") { result -> when (result) { is AdaptyResult.Success -> { val onboarding = result.value // Handle successful onboarding retrieval } is AdaptyResult.Error -> { val error = result.error // Handle error case } } } ``` Parameters: | Parameter | Presence | Description | |---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the onboarding localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.

| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

Adapty SDK stores onboardings locally in two layers: regularly updated cache described above and fallback onboardings. We also use CDN to fetch onboardings faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your onboardings while ensuring reliability even in cases where internet connection is scarce.

| --- # File: android-present-onboardings --- --- title: "Present onboardings in Android SDK" description: "Learn how to present onboardings on Android for effective user engagement." --- Before you start, ensure that: 1. You have installed [Adapty Android SDK](sdk-installation-android.md) 3.8.0 or later. 2. You have [created an onboarding](create-onboarding.md). 3. You have added the onboarding to a [placement](placements.md). If you've customized an onboarding using the Onboarding Builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such an onboarding contains both what should be shown and how it should be shown. In order to display the visual onboarding on the device screen, you must first configure it. To do this, call the method `AdaptyUI.getOnboardingView()` or create the `OnboardingView` directly: ```kotlin val onboardingView = AdaptyUI.getOnboardingView( activity = this, viewConfig = onboardingConfig, eventListener = eventListener ) ``` ```kotlin val onboardingView = AdaptyOnboardingView(activity) onboardingView.show( viewConfig = onboardingConfig, delegate = eventListener ) ``` ```java AdaptyOnboardingView onboardingView = AdaptyUI.getOnboardingView( activity, onboardingConfig, eventListener ); ``` ```java AdaptyOnboardingView onboardingView = new AdaptyOnboardingView(activity); onboardingView.show(onboardingConfig, eventListener); ``` ```xml ``` After the view has been successfully created, you can add it to the view hierarchy and display it on the device screen. Request parameters: | Parameter | Presence | Description | | :-------- | :------- |:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **viewConfig** | required | The onboarding configuration obtained from `AdaptyUI.getOnboardingConfiguration()` | | **eventListener** | required | An implementation of `AdaptyOnboardingEventListener` to handle onboarding events. Refer to [Handling onboarding events](android-handle-onboarding-events) for more details. | ## Change loading indicator color You can override the default color of the loading indicator in the following way: ```xml ``` ## Add smooth transitions between the splash screen and onboarding By default, between the splash screen and onboarding, you will see the loading screen until the onboarding is fully loaded. However, if you want to make the transition smoother, you can customize it and either extend the splash screen or display something else. To do this, create `adapty_onboarding_placeholder_view.xml` in `res/layout` and define a placeholder (what exactly will be shown while the onboarding is being loaded) there. If you define a placeholder, the onboarding will be loaded in the background and automatically displayed once ready. ## Disable safe area paddings By default, the onboarding view automatically applies safe area paddings to avoid system UI elements like status bar and navigation bar. However, if you want to disable this behavior and have full control over the layout, you can do so by setting the `safeAreaPaddings` parameter to `false`. ```kotlin val onboardingView = AdaptyUI.getOnboardingView( activity = this, viewConfig = onboardingConfig, eventListener = eventListener, safeAreaPaddings = false ) ``` ```kotlin val onboardingView = AdaptyOnboardingView(activity) onboardingView.show( viewConfig = onboardingConfig, delegate = eventListener, safeAreaPaddings = false ) ``` ```java AdaptyOnboardingView onboardingView = AdaptyUI.getOnboardingView( activity, onboardingConfig, eventListener, false ); ``` ```java AdaptyOnboardingView onboardingView = new AdaptyOnboardingView(activity); onboardingView.show(onboardingConfig, eventListener, false); ``` Alternatively, you can control this behavior globally by adding a boolean resource to your app: ```xml false ``` When `safeAreaPaddings` is set to `false`, the onboarding will extend to the full screen without any automatic padding adjustments, giving you complete control over the layout and allowing the onboarding content to use the entire screen space. ## Customize how links open in onboardings :::important Customizing how links open in onboardings is supported starting from Adapty SDK v. 3.15.1. ::: By default, links in onboardings open in an in-app browser. This provides a seamless user experience by displaying web pages within your application, allowing users to view them without switching apps. If you prefer to open links in an external browser instead, you can customize this behavior by setting the `externalUrlsPresentation` parameter to `AdaptyWebPresentation.ExternalBrowser`: ```kotlin val onboardingConfig = AdaptyUI.getOnboardingConfiguration( onboarding = onboarding, externalUrlsPresentation = AdaptyWebPresentation.ExternalBrowser // default – InAppBrowser ) ``` ```java AdaptyOnboardingConfiguration onboardingConfig = AdaptyUI.getOnboardingConfiguration( onboarding, AdaptyWebPresentation.ExternalBrowser // default – InAppBrowser ); ``` --- # File: android-handle-onboarding-events --- --- title: "Handle onboarding events in Android SDK" description: "Handle onboarding-related events in Android using Adapty." --- Before you start, ensure that: 1. You have installed [Adapty Android SDK](sdk-installation-android.md) 3.8.0 or later. 2. You have [created an onboarding](create-onboarding.md). 3. You have added the onboarding to a [placement](placements.md). Onboardings configured with the builder generate events your app can respond to. Learn how to respond to these events below. To control or monitor processes occurring on the onboarding screen within your Android app, implement the `AdaptyOnboardingEventListener` interface. ## Custom actions In the builder, you can add a **custom** action to a button and assign it an ID. Then, you can use this ID in your code and handle it as a custom action. For example, if a user taps a custom button, like **Login** or **Allow notifications**, the delegate method `onCustomAction` will be triggered with the action ID from the builder. You can create your own IDs, like "allowNotifications". ```kotlin showLineNumbers class YourActivity : AppCompatActivity() { private val eventListener = object : AdaptyOnboardingEventListener { override fun onCustomAction(action: AdaptyOnboardingCustomAction, context: Context) { when (action.actionId) { "allowNotifications" -> { // Request notification permissions } } } override fun onError(error: AdaptyOnboardingError, context: Context) { // Handle errors } // ... other required delegate methods } } ```
Event example (Click to expand) ```json { "actionId": "allowNotifications", "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "screensTotal": 3 } } ```
## Closing onboarding Onboarding is considered closed when a user taps a button with the **Close** action assigned. You need to manage what happens when a user closes the onboarding. For example: :::important You need to manage what happens when a user closes the onboarding. For instance, you need to stop displaying the onboarding itself. ::: For example: ```kotlin override fun onCloseAction(action: AdaptyOnboardingCloseAction, context: Context) { // Dismiss the onboarding screen (context as? Activity)?.onBackPressed() } ```
Event example (Click to expand) ```json { "action_id": "close_button", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "final_screen", "screen_index": 3, "total_screens": 4 } } ```
## Opening a paywall :::tip Handle this event to open a paywall if you want to open it inside the onboarding. If you want to open a paywall after it is closed, there is a more straightforward way to do it – handle [`AdaptyOnboardingCloseAction`](#closing-onboarding) and open a paywall without relying on the event data. ::: If a user clicks a button that opens a paywall, you will get a button action ID that you [set up manually](get-paid-in-onboardings.md). The most seamless way to work with paywalls in onboardings is to make the action ID equal to a paywall placement ID. This way, after the `AdaptyOnboardingOpenPaywallAction`, you can use the placement ID to get and open the paywall right away: ```kotlin override fun onOpenPaywallAction(action: AdaptyOnboardingOpenPaywallAction, context: Context) { // Get the paywall using the placement ID from the action Adapty.getPaywall(placementId = action.actionId) { result -> when (result) { is AdaptyResult.Success -> { val paywall = result.value // Get the paywall configuration AdaptyUI.getViewConfiguration(paywall) { result -> when(result) { is AdaptyResult.Success -> { val paywallConfig = result.value // Create and present the paywall val paywallView = AdaptyUI.getPaywallView( activity = this, viewConfig = paywallConfig, products, eventListener = paywallEventListener ) // Add the paywall view to your layout binding.container.addView(paywallView) } is AdaptyResult.Error -> { val error = result.error // handle the error } } } is AdaptyResult.Error -> { val error = result.error // handle the error } } } } ```
Event example (Click to expand) ```json { "action_id": "premium_offer_1", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "pricing_screen", "screen_index": 2, "total_screens": 4 } } ```
## Finishing loading onboarding When an onboarding finishes loading, this method will be invoked: ```kotlin override fun onFinishLoading(action: AdaptyOnboardingLoadedAction, context: Context) { // Handle loading completion } ```
Event example (Click to expand) ```json { "meta": { "onboarding_id": "onboarding_123", "screen_cid": "welcome_screen", "screen_index": 0, "total_screens": 4 } } ```
## Navigation events The `onAnalyticsEvent` method is called when various analytics events occur during the onboarding flow. The `event` object can be one of the following types: |Type | Description | |------------|-------------| | `OnboardingStarted` | When the onboarding has been loaded | | `ScreenPresented` | When any screen is shown | | `ScreenCompleted` | When a screen is completed. Includes optional `elementId` (identifier of the completed element) and optional `reply` (response from the user). Triggered when users perform any action to exit the screen. | | `SecondScreenPresented` | When the second screen is shown | | `UserEmailCollected` | Triggered when the user's email is collected via the input field | | `OnboardingCompleted` | Triggered when a user reaches a screen with the `final` ID. If you need this event, assign the `final` ID to the last screen. | | `Unknown` | For any unrecognized event type. Includes `name` (the name of the unknown event) and `meta` (additional metadata) | Each event includes `meta` information containing: | Field | Description | |------------|-------------| | `onboardingId` | Unique identifier of the onboarding flow | | `screenClientId` | Identifier of the current screen | | `screenIndex` | Current screen's position in the flow | | `totalScreens` | Total number of screens in the flow | Here's an example of how you can use analytics events for tracking: ```kotlin override fun onAnalyticsEvent(event: AdaptyOnboardingAnalyticsEvent, context: Context) { when (event) { is AdaptyOnboardingAnalyticsEvent.OnboardingStarted -> { // Track onboarding start trackEvent("onboarding_started", event.meta) } is AdaptyOnboardingAnalyticsEvent.ScreenPresented -> { // Track screen presentation trackEvent("screen_presented", event.meta) } is AdaptyOnboardingAnalyticsEvent.ScreenCompleted -> { // Track screen completion with user response trackEvent("screen_completed", event.meta, event.elementId, event.reply) } is AdaptyOnboardingAnalyticsEvent.OnboardingCompleted -> { // Track successful onboarding completion trackEvent("onboarding_completed", event.meta) } is AdaptyOnboardingAnalyticsEvent.Unknown -> { // Handle unknown events trackEvent(event.name, event.meta) } // Handle other cases as needed } } ```
Event examples (Click to expand) ```javascript // OnboardingStarted { "name": "onboarding_started", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "welcome_screen", "screen_index": 0, "total_screens": 4 } } // ScreenPresented { "name": "screen_presented", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "interests_screen", "screen_index": 2, "total_screens": 4 } } // ScreenCompleted { "name": "screen_completed", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 1, "total_screens": 4 }, "params": { "element_id": "profile_form", "reply": "success" } } // SecondScreenPresented { "name": "second_screen_presented", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 1, "total_screens": 4 } } // UserEmailCollected { "name": "user_email_collected", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 1, "total_screens": 4 } } // OnboardingCompleted { "name": "onboarding_completed", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "final_screen", "screen_index": 3, "total_screens": 4 } } ```
--- # File: android-onboarding-input --- --- title: "Process data from onboardings in Android SDK" description: "Save and use data from onboardings in your Android app with Adapty SDK." --- When your users respond to a quiz question or input their data into an input field, the `onStateUpdatedAction` method will be invoked. You can save or process the field type in your code. For example: ```kotlin override fun onStateUpdatedAction(action: AdaptyOnboardingStateUpdatedAction, context: Context) { // Store user preferences or responses when (val params = action.params) { is AdaptyOnboardingStateUpdatedParams.Select -> { // Handle single selection } is AdaptyOnboardingStateUpdatedParams.MultiSelect -> { // Handle multiple selections } is AdaptyOnboardingStateUpdatedParams.Input -> { // Handle text input } is AdaptyOnboardingStateUpdatedParams.DatePicker -> { // Handle date selection } } } ``` See the action format [here](https://android.adapty.io/adapty-ui/com.adapty.ui.onboardings.actions/-adapty-onboarding-state-updated-action/).
Saved data examples (the format may differ in your implementation) ```javascript // Example of a saved select action { "elementId": "preference_selector", "meta": { "onboardingId": "onboarding_123", "screenClientId": "preferences_screen", "screenIndex": 1, "screensTotal": 3 }, "params": { "type": "select", "value": { "id": "option_1", "value": "premium", "label": "Premium Plan" } } } // Example of a saved multi-select action { "elementId": "interests_selector", "meta": { "onboardingId": "onboarding_123", "screenClientId": "interests_screen", "screenIndex": 2, "screensTotal": 3 }, "params": { "type": "multiSelect", "value": [ { "id": "interest_1", "value": "sports", "label": "Sports" }, { "id": "interest_2", "value": "music", "label": "Music" } ] } } // Example of a saved input action { "elementId": "name_input", "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "screensTotal": 3 }, "params": { "type": "input", "value": { "type": "text", "value": "John Doe" } } } // Example of a saved date picker action { "elementId": "birthday_picker", "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "screensTotal": 3 }, "params": { "type": "datePicker", "value": { "day": 15, "month": 6, "year": 1990 } } } ```
## Use cases ### Enrich user profiles with data If you want to immediately link the input data with the user profile and avoid asking them twice for the same info, you need to [update the user profile](android-setting-user-attributes.md) with the input data when handling the action. For example, you ask users to enter their name in the text field with the `name` ID, and you want to set this field's value as user's first name. Also, you ask them to enter their email in the `email` field. In your app code, it can look like this: ```kotlin showLineNumbers override fun onStateUpdatedAction(action: AdaptyOnboardingStateUpdatedAction, context: Context) { // Store user preferences or responses when (val params = action.params) { is AdaptyOnboardingStateUpdatedParams.Input -> { // Handle text input val builder = AdaptyProfileParameters.Builder() // Map elementId to appropriate profile field when (action.elementId) { "name" -> { when (val inputParams = params.params) { is AdaptyOnboardingInputParams.Text -> { builder.withFirstName(inputParams.value) } } } "email" -> { when (val inputParams = params.params) { is AdaptyOnboardingInputParams.Email -> { builder.withEmail(inputParams.value) } } } } Adapty.updateProfile(builder.build()) { error -> if (error != null) { // handle the error } } } } } ``` ### Customize paywalls based on answers Using quizzes in onboardings, you can also customize paywalls you show users after they complete the onboarding. For example, you can ask users about their experience with sport and show different CTAs and products to different user groups. 1. [Add a quiz](onboarding-quizzes.md) in the onboarding builder and assign meaningful IDs to its options. 2. Handle the quiz responses based on their IDs and [set custom attributes](android-setting-user-attributes.md) for users. ```kotlin showLineNumbers override fun onStateUpdatedAction(action: AdaptyOnboardingStateUpdatedAction, context: Context) { // Handle quiz responses and set custom attributes when (val params = action.params) { is AdaptyOnboardingStateUpdatedParams.Select -> { // Handle quiz selection val builder = AdaptyProfileParameters.Builder() // Map quiz responses to custom attributes when (action.elementId) { "experience" -> { // Set custom attribute 'experience' with the selected value (beginner, amateur, pro) builder.withCustomAttribute("experience", params.params.value) } } Adapty.updateProfile(builder.build()) { error -> if (error != null) { // handle the error } } } } } ``` 3. [Create segments](segments.md) for each custom attribute value. 4. Create a [placement](placements.md) and add [audiences](audience.md) for each segment you've created. 5. [Display a paywall](android-paywalls.md) for the placement in your app code. If your onboarding has a button that opens a paywall, implement the paywall code as a [response to this button's action](android-handle-onboarding-events#opening-a-paywall). --- # File: android-test --- --- title: "Test & release in Android SDK" description: "Learn how to check subscription status in your Android app with Adapty." --- If you've already implemented the Adapty SDK in your Android app, you'll want to test that everything is set up correctly and that purchases work as expected. This involves testing both the SDK integration and the actual purchase flow with Google Play's sandbox environment. ## Test your app For comprehensive testing of your in-app purchases, including sandbox testing and closed track validation, see our [testing guide](testing-on-android.md). ## Prepare for release Before submitting your app to the store, follow the [Release checklist](release-checklist) to confirm: - Store connection and server notifications are configured - Purchases complete and are reported to Adapty - Access unlocks and restores correctly - Privacy and review requirements are met --- # File: android-sdk-error-handling --- --- title: "Handle errors in Android SDK" description: "Handle Android SDK errors effectively with Adapty’s troubleshooting guide." --- Every error is returned by the SDK is `AdaptyError`. :::important If these solutions don't resolve your issue, see [Other issues](#other-issues) for steps to take before contacting support to help us assist you more efficiently. ::: | Error | Solution | |----------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | UNKNOWN | This error indicates that an unknown or unexpected error occurred. | | [ITEM_UNAVAILABLE](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponseCode#ITEM_UNAVAILABLE()) | This error mostly happens at the testing stage. It may mean that the products are absent from production or that the user does not belong to the Testers group in Google Play. | | ADAPTY_NOT_INITIALIZED | The Adapty SDK is not activated. You need to properly [configure Adapty SDK](sdk-installation-android#configure-adapty-sdk) using the `Adapty.activate` method. | | PRODUCT_NOT_FOUND | This error indicates that the product requested for purchase is not available in the store. | | INVALID_JSON |

The local fallback paywall JSON is not valid.

Fix your default English paywall, after that replace invalid local paywalls. Refer to the [Customize paywall with remote config](customize-paywall-with-remote-config) topic for details on how to fix a paywall and to the [Define local fallback paywalls](fallback-paywalls) for details on how to replace the local paywalls.

| |

CURRENT_SUBSCRIPTION_TO_UPDATE

\_NOT_FOUND_IN_HISTORY

| The original subscription that needs to be replaced is not found in active subscriptions. | | [BILLING_SERVICE_TIMEOUT](https://developer.android.com/google/play/billing/errors#service_timeout_error_code_-3) | This error indicates that the request has reached the maximum timeout before Google Play can respond. This could be caused, for example, by a delay in the execution of the action requested by the Play Billing Library call. | | [FEATURE_NOT_SUPPORTED](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponseCode#FEATURE_NOT_SUPPORTED()) | The requested feature is not supported by the Play Store on the current device. | | [BILLING_SERVICE_DISCONNECTED](https://developer.android.com/google/play/billing/errors#service_disconnected_error_code_-1) | This error indicates that the client app’s connection to the Google Play Store service via the `BillingClient` has been severed. | | [BILLING_SERVICE_UNAVAILABLE](https://developer.android.com/google/play/billing/errors#service_unavailable_error_code_2) | This error indicates the Google Play Billing service is currently unavailable. In most cases, this means there is a network connection issue anywhere between the client device and Google Play Billing services. | | [BILLING_UNAVAILABLE](https://developer.android.com/google/play/billing/errors#billing_unavailable_error_code_3) |

This error indicates a billing issue occurred during the purchase process. Possible reasons include:

1. The Play Store app on the user's device is missing or outdated.

2. The user is in an unsupported country.

3. The user is part of an enterprise account where the admin has disabled purchases.

4. Google Play couldn't charge the user's payment method (e.g., an expired credit card).

5. The user is not logged into the Play Store app.

| | [DEVELOPER_ERROR](https://developer.android.com/google/play/billing/errors#developer_error) | This error indicates you're improperly using an API. | | [BILLING_ERROR](https://developer.android.com/google/play/billing/errors#error_error_code_6) | This error indicates an internal problem with Google Play itself. | | [ITEM_ALREADY_OWNED](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponseCode#ITEM_ALREADY_OWNED()) | The product has already been purchased. | | [ITEM_NOT_OWNED](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponseCode#ITEM_NOT_OWNED()) | This error indicates that the requested action on the item failed since it is not owned by the user. | | [BILLING_NETWORK_ERROR](https://developer.android.com/google/play/billing/errors#network_error_error_code_12) | This error indicates that there was a problem with the network connection between the device and Play systems. | | NO_PRODUCT_IDS_FOUND |

This error indicates that none of the products in the paywall is available in the store.

If you are encountering this error, please follow the steps below to resolve it:

  1. Check if all the products have been added to Adapty Dashboard.
  2. Ensure that the **Package name** of your app matches the one from the Google Play Console.
  3. Verify that the product identifiers from the app stores match those you have added to the Dashboard. Please note that the identifiers should not contain Bundle ID, unless it is already included in the store.
  4. Confirm that the app paid status is **Active** in your Google tax settings. Ensure that your tax information is up-to-date and your certificates are valid.
  5. Check if a bank account is attached to the app, so it can be eligible for monetization.
  6. Check if the products are available in your region.
  7. Ensure your app is in one of the testing tracks. The **Internal testing** track is the easiest option since it doesn’t require a review and keeps the app hidden from customers.
| | NO_PURCHASES_TO_RESTORE | This error indicates that Google Play did not find the purchase to restore. | | AUTHENTICATION_ERROR | You need to properly [configure Adapty SDK](sdk-installation-android#configure-adapty-sdk) by `Adapty.activate` method. | | BAD_REQUEST | Bad request.
Ensure you've completed all the steps required to [integrate with Google Play](google-play-store-connection-configuration). | | SERVER_ERROR | Server error. | | REQUEST_FAILED | This error indicates a network issue that cannot be properly defined. | | DECODING_FAILED | We could not decode the response.
Review your code and ensure that you the parameters you send are valid. For example, this error can indicate that you're using an invalid API key. | | ANALYTICS_DISABLED | We can't handle analytics events, since you've [opted it out](analytics-integration#disabling-external-analytics-for-a-specific-customer). | | WRONG_PARAMETER | This error indicates that some of your parameters are not correct: blank when it cannot be blank or wrong type, etc. | ## Other issues If you haven't found a solution yet, the next steps can be: - **Upgrading the SDK to the latest version**: We always recommend upgrading to the latest SDK versions since they are more stable and include fixes for known issues. - **Contact the support team or get help from your fellow developers** in the [support forum](https://adapty.featurebase.app/). - **Contact the support team via [support@adapty.io](mailto:support@adapty.io) or via the chat**: If you are not ready to upgrade the SDK or it didn't help, contact our support team. Note that your issue will be resolved faster if you [enable verbose logging](sdk-installation-android#logging) and share logs with the team. You can also attach relevant code snippets. --- # File: migration-to-android-312 --- --- title: "Migrate Adapty Android SDK to v. 3.12" description: "Migrate to Adapty Android SDK v3.12 for better performance and new monetization features." --- In Adapty SDK 3.12.0, we have deleted the `logShowOnboarding` method from the SDK. If you have been using this method, it won't be available when you upgrade the SDK to version 3.12 or later. Instead, you can [create onboardings in the Adapty no-code onboarding builder](onboardings.md). Analytics for these onboardings are tracked automatically, and you have a lot of customization options. --- # File: migration-to-android-310 --- --- title: "Migration guide to Android Adapty SDK 3.10.0" description: "" --- Adapty SDK 3.10.0 is a major release that brought some improvements that however may require some migration steps from you: 1. `AdaptyUiPersonalizedOfferResolver` has been removed. If you are using it, pass it in the `onAwaitingPurchaseParams` callback. 2. Update the `onAwaitingSubscriptionUpdateParams` method signature for Paywall Builder paywalls. ## Update purchase parameters callback The `onAwaitingSubscriptionUpdateParams` method has been renamed to `onAwaitingPurchaseParams` and now uses `AdaptyPurchaseParameters` instead of `AdaptySubscriptionUpdateParameters`. This allows you to specify subscription replacement parameters (crossgrade) and indicate whether the price is personalized ([read more](https://developer.android.com/google/play/billing/integrate#personalized-price)), along with other purchase parameters. ```diff showLineNumbers - override fun onAwaitingSubscriptionUpdateParams( - product: AdaptyPaywallProduct, - context: Context, - onSubscriptionUpdateParamsReceived: SubscriptionUpdateParamsCallback, - ) { - onSubscriptionUpdateParamsReceived(AdaptySubscriptionUpdateParameters(...)) - } + override fun onAwaitingPurchaseParams( + product: AdaptyPaywallProduct, + context: Context, + onPurchaseParamsReceived: AdaptyUiEventListener.PurchaseParamsCallback, + ): AdaptyUiEventListener.PurchaseParamsCallback.IveBeenInvoked { + onPurchaseParamsReceived( + AdaptyPurchaseParameters.Builder() + .withSubscriptionUpdateParams(AdaptySubscriptionUpdateParameters(...)) + .withOfferPersonalized(true) + .build() + ) + return AdaptyUiEventListener.PurchaseParamsCallback.IveBeenInvoked + } ``` If no additional parameters are needed, you can simply use: ```kotlin showLineNumbers + override fun onAwaitingPurchaseParams( product: AdaptyPaywallProduct, context: Context, onPurchaseParamsReceived: AdaptyUiEventListener.PurchaseParamsCallback, ): AdaptyUiEventListener.PurchaseParamsCallback.IveBeenInvoked { onPurchaseParamsReceived(AdaptyPurchaseParameters.Empty) return AdaptyUiEventListener.PurchaseParamsCallback.IveBeenInvoked } ``` --- # File: migration-to-android-sdk-34 --- --- title: "Migrate Adapty Android SDK to v. 3.4" description: "Migrate to Adapty Android SDK v3.4 for better performance and new monetization features." --- Adapty SDK 3.4.0 is a major release that introduces improvements that require migration steps on your end. ## Update fallback paywall files Update your fallback paywall files to ensure compatibility with the new SDK version: 1. [Download the updated fallback paywall files](fallback-paywalls#download-fallback-paywalls-as-a-file-in-the-adapty-dashboard) from the Adapty Dashboard. 2. [Replace the existing fallback paywalls in your mobile app](android-use-fallback-paywalls) with the new files. ## Update implementation of Observer Mode If you're using Observer Mode, make sure to update its implementation. In previous versions, you had to restore purchases so Adapty could recognize transactions made through your own infrastructure, as Adapty had no direct access to them in Observer Mode. If you used paywalls, you also needed to manually associate each transaction with the paywall that initiated it. In the new version, you must explicitly report each transaction for Adapty to recognize it. If you use paywalls, you also need to pass the variation ID to link the transaction to the paywall used. :::warning **Don't skip transaction reporting!** If you don't call `reportTransaction`, Adapty won't recognize the transaction, it won't appear in analytics, and it won't be sent to integrations. ::: ```diff showLineNumbers - Adapty.restorePurchases { result -> - if (result is AdaptyResult.Success) { - // success - } - } - - Adapty.setVariationId(transactionId, variationId) { error -> - if (error == null) { - // success - } - } + val transactionInfo = TransactionInfo.fromPurchase(purchase) + + Adapty.reportTransaction(transactionInfo, variationId) { result -> + if (result is AdaptyResult.Success) { + // success + } + } ``` ```diff showLineNumbers - Adapty.restorePurchases(result -> { - if (result instanceof AdaptyResult.Success) { - // success - } - }); - - Adapty.setVariationId(transactionId, variationId, error -> { - if (error == null) { - // success - } - }); + TransactionInfo transactionInfo = TransactionInfo.fromPurchase(purchase); + + Adapty.reportTransaction(transactionInfo, variationId, result -> { + if (result instanceof AdaptyResult.Success) { + // success + } + }); ``` --- # File: migration-to-android330 --- --- title: "Migrate Adapty Android SDK to v. 3.3" description: "Migrate to Adapty Android SDK v3.3 for better performance and new monetization features." --- Adapty SDK 3.3.0 is a major release that brought some improvements which however may require some migration steps from you. 1. Update how you handle making purchases in paywalls not created with Paywall Builder. Stop processing the `USER_CANCELED` and `PENDING_PURCHASE` error codes. A canceled purchase is no longer considered an error and will now appear in the non-error purchase results. 2. Replace the `onPurchaseCanceled` and `onPurchaseSuccess` events with the new `onPurchaseFinished` event for paywalls created with Paywall Builder. This change is for the same reason: canceled purchases are no longer treated as errors and will be included in the non-error purchase results. 3. Change the method signature of `onAwaitingSubscriptionUpdateParams` for Paywall Builder paywalls. 4. Update the method used to provide fallback paywalls if you pass the file URI directly. 5. Update the integration configurations for for Adjust, AirBridge, Amplitude, AppMetrica, Appsflyer, Branch, Facebook Ads, Firebase and Google Analytics, Mixpanel, OneSignal, Pushwoosh. ## Update making purchase Previously canceled and pending purchases were considered errors and returned the `USER_CANCELED` and `PENDING_PURCHASE` codes, respectively. Now a new `AdaptyPurchaseResult` class is used to indicate canceled, successful, and pending purchases. Update the code of purchasing in the following way: ~~~diff Adapty.makePurchase(activity, product) { result -> when (result) { is AdaptyResult.Success -> { - val info = result.value - val profile = info?.profile - - if (profile?.accessLevels?.get("YOUR_ACCESS_LEVEL")?.isActive == true) { - // Grant access to the paid features - } + when (val purchaseResult = result.value) { + is AdaptyPurchaseResult.Success -> { + val profile = purchaseResult.profile + if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive == true) { + // Grant access to the paid features + } + } + + is AdaptyPurchaseResult.UserCanceled -> { + // Handle the case where the user canceled the purchase + } + + is AdaptyPurchaseResult.Pending -> { + // Handle deferred purchases (e.g., the user will pay offline with cash + } + } } is AdaptyResult.Error -> { val error = result.error // Handle the error } } } ~~~ For the complete code example, check out the [Make purchases in mobile app](android-making-purchases#make-purchase) page. ## Modify Paywall Builder purchase events 1. Add `onPurchaseFinished` event: ```diff showLineNumbers + public override fun onPurchaseFinished( + purchaseResult: AdaptyPurchaseResult, + product: AdaptyPaywallProduct, + context: Context, + ) { + when (purchaseResult) { + is AdaptyPurchaseResult.Success -> { + // Grant access to the paid features + } + is AdaptyPurchaseResult.UserCanceled -> { + // Handle the case where the user canceled the purchase + } + is AdaptyPurchaseResult.Pending -> { + // Handle deferred purchases (e.g., the user will pay offline with cash) + } + } + } ``` For the complete code example, check out the [Successful, canceled, or pending purchase](android-handling-events#successful-canceled-or-pending-purchase) and event description. 2. Remove processing of the `onPurchaseCancelled` event: ```diff showLineNumbers - public override fun onPurchaseCanceled( - product: AdaptyPaywallProduct, - context: Context, - ) {} ``` 3. Remove `onPurchaseSuccess`: ```diff showLineNumbers - public override fun onPurchaseSuccess( - profile: AdaptyProfile?, - product: AdaptyPaywallProduct, - context: Context, - ) { - // Your logic on successful purchase - } ``` ## Change the signature of onAwaitingSubscriptionUpdateParams method Now if a new subscription is purchased while another is still active, call `onSubscriptionUpdateParamsReceived(AdaptySubscriptionUpdateParameters...))` if the new subscription should replace a currently active subscription or `onSubscriptionUpdateParamsReceived(null)` if the active subscription should remain active and the new one should be added separately: ```diff showLineNumbers - public override fun onAwaitingSubscriptionUpdateParams( - product: AdaptyPaywallProduct, - context: Context, - ): AdaptySubscriptionUpdateParameters? { - return AdaptySubscriptionUpdateParameters(...) - } + public override fun onAwaitingSubscriptionUpdateParams( + product: AdaptyPaywallProduct, + context: Context, + onSubscriptionUpdateParamsReceived: SubscriptionUpdateParamsCallback, + ) { + onSubscriptionUpdateParamsReceived(AdaptySubscriptionUpdateParameters(...)) + } ``` See the [Upgrade subscription](android-handling-events#upgrade-subscription) doc section for the final code example. ## Update providing fallback paywalls If you pass file URI to provide fallback paywalls, update how you do it in the following way: ```diff showLineNumbers val fileUri: Uri = // Get the URI for the file with fallback paywalls - Adapty.setFallbackPaywalls(fileUri, callback) + Adapty.setFallbackPaywalls(FileLocation.fromFileUri(fileUri), callback) ``` ```diff showLineNumbers Uri fileUri = // Get the URI for the file with fallback paywalls - Adapty.setFallbackPaywalls(fileUri, callback); + Adapty.setFallbackPaywalls(FileLocation.fromFileUri(fileUri), callback); ``` ## Update third-party integration SDK configuration To ensure integrations work properly with Adapty Android SDK 3.3.0 and later, update your SDK configurations for the following integrations as described in the sections below. ### Adjust Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Adjust integration](adjust#sdk-configuration). ```diff showLineNumbers - Adjust.getAttribution { attribution -> - if (attribution == null) return@getAttribution - - Adjust.getAdid { adid -> - if (adid == null) return@getAdid - - Adapty.updateAttribution(attribution, AdaptyAttributionSource.ADJUST, adid) { error -> - // Handle the error - } - } - } + Adjust.getAdid { adid -> + if (adid == null) return@getAdid + + Adapty.setIntegrationIdentifier("adjust_device_id", adid) { error -> + if (error != null) { + // Handle the error + } + } + } + + Adjust.getAttribution { attribution -> + if (attribution == null) return@getAttribution + + Adapty.updateAttribution(attribution, "adjust") { error -> + if (error != null) { + // Handle the error + } + } + } ``` ```diff showLineNumbers val config = AdjustConfig(context, adjustAppToken, environment) config.setOnAttributionChangedListener { attribution -> attribution?.let { attribution -> - Adapty.updateAttribution(attribution, AdaptyAttributionSource.ADJUST) { error -> + Adapty.updateAttribution(attribution, "adjust") { error -> if (error != null) { // Handle the error } } } } Adjust.onCreate(config) ``` ### AirBridge Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for AirBridge integration](airbridge#sdk-configuration). ```diff showLineNumbers Airbridge.getDeviceInfo().getUUID(object: AirbridgeCallback.SimpleCallback() { override fun onSuccess(result: String) { - val params = AdaptyProfileParameters.Builder() - .withAirbridgeDeviceId(result) - .build() - Adapty.updateProfile(params) { error -> - if (error != null) { - // Handle the error - } - } + Adapty.setIntegrationIdentifier("airbridge_device_id", result) { error -> + if (error != null) { + // Handle the error + } + } } override fun onFailure(throwable: Throwable) { } }) ``` ### Amplitude Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Amplitude integration](amplitude#sdk-configuration). ```diff showLineNumbers // For Amplitude maintenance SDK (obsolete) val amplitude = Amplitude.getInstance() val amplitudeDeviceId = amplitude.getDeviceId() val amplitudeUserId = amplitude.getUserId() //for actual Amplitude Kotlin SDK val amplitude = Amplitude( Configuration( apiKey = AMPLITUDE_API_KEY, context = applicationContext ) ) val amplitudeDeviceId = amplitude.store.deviceId val amplitudeUserId = amplitude.store.userId // - val params = AdaptyProfileParameters.Builder() - .withAmplitudeDeviceId(amplitudeDeviceId) - .withAmplitudeUserId(amplitudeUserId) - .build() - Adapty.updateProfile(params) { error -> - if (error != null) { - // Handle the error - } - } + Adapty.setIntegrationIdentifier("amplitude_user_id", amplitudeUserId) { error -> + if (error != null) { + // Handle the error + } + } + Adapty.setIntegrationIdentifier("amplitude_device_id", amplitudeDeviceId) { error -> + if (error != null) { + // Handle the error + } + } ``` ### AppMetrica Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for AppMetrica integration](appmetrica#sdk-configuration). ```diff showLineNumbers val startupParamsCallback = object: StartupParamsCallback { override fun onReceive(result: StartupParamsCallback.Result?) { val deviceId = result?.deviceId ?: return - val params = AdaptyProfileParameters.Builder() - .withAppmetricaDeviceId(deviceId) - .withAppmetricaProfileId("YOUR_ADAPTY_CUSTOMER_USER_ID") - .build() - Adapty.updateProfile(params) { error -> - if (error != null) { - // Handle the error - } - } + Adapty.setIntegrationIdentifier("appmetrica_device_id", deviceId) { error -> + if (error != null) { + // Handle the error + } + } + + Adapty.setIntegrationIdentifier("appmetrica_profile_id", "YOUR_ADAPTY_CUSTOMER_USER_ID") { error -> + if (error != null) { + // Handle the error + } + } } override fun onRequestError( reason: StartupParamsCallback.Reason, result: StartupParamsCallback.Result? ) { // Handle the error } } AppMetrica.requestStartupParams(context, startupParamsCallback, listOf(StartupParamsCallback.APPMETRICA_DEVICE_ID)) ``` ### AppsFlyer Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for AppsFlyer integration](appsflyer#sdk-configuration). ```diff showLineNumbers val conversionListener: AppsFlyerConversionListener = object : AppsFlyerConversionListener { override fun onConversionDataSuccess(conversionData: Map) { - Adapty.updateAttribution( - conversionData, - AdaptyAttributionSource.APPSFLYER, - AppsFlyerLib.getInstance().getAppsFlyerUID(context) - ) { error -> - if (error != null) { - // Handle the error - } - } + val uid = AppsFlyerLib.getInstance().getAppsFlyerUID(context) + Adapty.setIntegrationIdentifier("appsflyer_id", uid) { error -> + if (error != null) { + // Handle the error + } + } + Adapty.updateAttribution(conversionData, "appsflyer") { error -> + if (error != null) { + // Handle the error + } + } } } ``` ### Branch Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Branch integration](branch#sdk-configuration). ```diff showLineNumbers // Login and update attribution Branch.getAutoInstance(this) .setIdentity("YOUR_USER_ID") { referringParams, error -> referringParams?.let { data -> - Adapty.updateAttribution(data, AdaptyAttributionSource.BRANCH) { error -> - if (error != null) { - // Handle the error - } - } + Adapty.updateAttribution(data, "branch") { error -> + if (error != null) { + // Handle the error + } + } } } // Logout Branch.getAutoInstance(context).logout() ``` ### Facebook Ads Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Facebook Ads integration](facebook-ads#sdk-configuration). ```diff showLineNumbers - val builder = AdaptyProfileParameters.Builder() - .withFacebookAnonymousId(AppEventsLogger.getAnonymousAppDeviceGUID(context)) - - Adapty.updateProfile(builder.build()) { error -> - if (error != null) { - // Handle the error - } - } + Adapty.setIntegrationIdentifier( + "facebook_anonymous_id", + AppEventsLogger.getAnonymousAppDeviceGUID(context) + ) { error -> + if (error != null) { + // Handle the error + } + } ``` ### Firebase and Google Analytics Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Firebase and Google Analytics integration](firebase-and-google-analytics). ```diff showLineNumbers // After Adapty.activate() FirebaseAnalytics.getInstance(context).appInstanceId.addOnSuccessListener { appInstanceId -> - Adapty.updateProfile( - AdaptyProfileParameters.Builder() - .withFirebaseAppInstanceId(appInstanceId) - .build() - ) { error -> - if (error != null) { - // Handle the error - } - } + Adapty.setIntegrationIdentifier("firebase_app_instance_id", appInstanceId) { error -> + if (error != null) { + // Handle the error + } + } } ``` ```diff showLineNumbers // After Adapty.activate() - FirebaseAnalytics.getInstance(context).getAppInstanceId().addOnSuccessListener(appInstanceId -> { - AdaptyProfileParameters params = new AdaptyProfileParameters.Builder() - .withFirebaseAppInstanceId(appInstanceId) - .build(); - - Adapty.updateProfile(params, error -> { - if (error != null) { - // Handle the error - } - }); - }); + FirebaseAnalytics.getInstance(context).getAppInstanceId().addOnSuccessListener(appInstanceId -> { + Adapty.setIntegrationIdentifier("firebase_app_instance_id", appInstanceId, error -> { + if (error != null) { + // Handle the error + } + }); + }); ``` ### Mixpanel Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Mixpanel integration](mixpanel#sdk-configuration). ```diff showLineNumbers - val params = AdaptyProfileParameters.Builder() - .withMixpanelUserId(mixpanelAPI.distinctId) - .build() - - Adapty.updateProfile(params) { error -> - if (error != null) { - // Handle the error - } - } + Adapty.setIntegrationIdentifier("mixpanel_user_id", mixpanelAPI.distinctId) { error -> + if (error != null) { + // Handle the error + } + } ``` ### OneSignal Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for OneSignal integration](onesignal#sdk-configuration). ```diff showLineNumbers // SubscriptionID val oneSignalSubscriptionObserver = object: IPushSubscriptionObserver { override fun onPushSubscriptionChange(state: PushSubscriptionChangedState) { - val params = AdaptyProfileParameters.Builder() - .withOneSignalSubscriptionId(state.current.id) - .build() - - Adapty.updateProfile(params) { error -> + Adapty.setIntegrationIdentifier("one_signal_subscription_id", state.current.id) { error -> if (error != null) { // Handle the error } } } } ``` ```diff showLineNumbers // SubscriptionID IPushSubscriptionObserver oneSignalSubscriptionObserver = state -> { - AdaptyProfileParameters params = new AdaptyProfileParameters.Builder() - .withOneSignalSubscriptionId(state.getCurrent().getId()) - .build(); - Adapty.updateProfile(params, error -> { + Adapty.setIntegrationIdentifier("one_signal_subscription_id", state.getCurrent().getId(), error -> { if (error != null) { // Handle the error } }); }; ``` ```diff showLineNumbers // PlayerID val osSubscriptionObserver = OSSubscriptionObserver { stateChanges -> stateChanges?.to?.userId?.let { playerId -> - val params = AdaptyProfileParameters.Builder() - .withOneSignalPlayerId(playerId) - .build() - - Adapty.updateProfile(params) { error -> + Adapty.setIntegrationIdentifier("one_signal_player_id", playerId) { error -> if (error != null) { // Handle the error } - } } } ``` ```diff showLineNumbers // PlayerID OSSubscriptionObserver osSubscriptionObserver = stateChanges -> { OSSubscriptionState to = stateChanges != null ? stateChanges.getTo() : null; String playerId = to != null ? to.getUserId() : null; if (playerId != null) { - AdaptyProfileParameters params1 = new AdaptyProfileParameters.Builder() - .withOneSignalPlayerId(playerId) - .build(); - - Adapty.updateProfile(params1, error -> { + Adapty.setIntegrationIdentifier("one_signal_player_id", playerId, error -> { if (error != null) { // Handle the error } - }); } }; ``` ### Pushwoosh Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Pushwoosh integration](pushwoosh#sdk-configuration). ```diff showLineNumbers - val params = AdaptyProfileParameters.Builder() - .withPushwooshHwid(Pushwoosh.getInstance().hwid) - .build() - Adapty.updateProfile(params) { error -> + Adapty.setIntegrationIdentifier("pushwoosh_hwid", Pushwoosh.getInstance().hwid) { error -> if (error != null) { // Handle the error } } ``` ```diff showLineNumbers - AdaptyProfileParameters params = new AdaptyProfileParameters.Builder() - .withPushwooshHwid(Pushwoosh.getInstance().getHwid()) - .build(); - - Adapty.updateProfile(params, error -> { + Adapty.setIntegrationIdentifier("pushwoosh_hwid", Pushwoosh.getInstance().getHwid(), error -> { if (error != null) { // Handle the error } }); ``` --- # File: migration-to-android-sdk-v3 --- --- title: "Migrate Adapty Android SDK to v. 3.0" description: "Migrate to Adapty Android SDK v3.0 for better performance and new monetization features." --- Adapty SDK v.3.0 brings support for the new exciting [Adapty Paywall Builder](adapty-paywall-builder), the new version of the no-code user-friendly tool to create paywalls. With its maximum flexibility and rich design capabilities, your paywalls will become most effective and profitable. Adapty SDKs are delivered as a BoM (Bill of Materials), ensuring that the Adapty SDK and AdaptyUI SDK versions in your app remain consistent. To migrate to v3.0, update your code as follows: ```diff showLineNumbers dependencies { ... - implementation 'io.adapty:android-sdk:2.11.5' - implementation 'io.adapty:android-ui:2.11.3' + implementation platform('io.adapty:adapty-bom:3.0.4') + implementation 'io.adapty:android-sdk' + implementation 'io.adapty:android-ui' } ``` ```diff showLineNumbers dependencies { ... - implementation("io.adapty:android-sdk:2.11.5") - implementation("io.adapty:android-ui:2.11.3") + implementation(platform("io.adapty:adapty-bom:3.0.4")) + implementation("io.adapty:android-sdk") + implementation("io.adapty:android-ui") } ``` ```diff showLineNumbers //libs.versions.toml [versions] .. - adapty = "2.11.5" - adaptyUi = "2.11.3" + adaptyBom = "3.0.4" [libraries] .. - adapty = { group = "io.adapty", name = "android-sdk", version.ref = "adapty" } - adapty-ui = { group = "io.adapty", name = "android-ui", version.ref = "adaptyUi" } + adapty-bom = { module = "io.adapty:adapty-bom", version.ref = "adaptyBom" } + adapty = { module = "io.adapty:android-sdk" } + adapty-ui = { module = "io.adapty:android-ui" } //module-level build.gradle.kts dependencies { ... + implementation(libs.adapty.bom) implementation(libs.adapty) implementation(libs.adapty.ui) } ``` --- # File: android-legacy-install --- --- title: "Legacy installation guide" description: "Get started with Adapty on Android to streamline subscription setup and management." --- Please consult the compatibility table below to choose the correct pair of Adapty SDK and AdaptyUI SDK. | Adapty SDK version | AdaptyUI version | | :----------------- | :--------------- | | 2.7.x–2.9.x | 2.0.x | | 2.10.0 | 2.1.2 | | 2.10.2 | 2.1.3 | | 2.11.0 - 2.11.3 | 2.11.0 - 2.11.2 | | 2.11.5 | 2.11.3 | You can install Adapty SDK via Gradle. :::danger Go through release checklist before releasing your app Before releasing your application, make sure to carefully review the [Release Checklist](release-checklist) thoroughly. This checklist ensures that you've completed all necessary steps and provides criteria for evaluating the success of your integration. ::: ### Install via Gradle ```groovy showLineNumbers dependencies { ... implementation 'io.adapty:android-sdk:2.11.5' implementation 'io.adapty:android-ui:2.11.3' } ``` ```kotlin showLineNumbers dependencies { ... implementation("io.adapty:android-sdk:2.11.5") implementation("io.adapty:android-ui:2.11.3") } ``` ```toml showLineNumbers //libs.versions.toml [versions] .. adapty = "2.11.5" adaptyUi = "2.11.3" [libraries] .. adapty = { group = "io.adapty", name = "android-sdk", version.ref = "adapty" } adapty-ui = { group = "io.adapty", name = "android-ui", version.ref = "adaptyUi" } //module-level build.gradle.kts dependencies { ... implementation(libs.adapty) implementation(libs.adapty.ui) } ``` If the dependency is not being resolved, please make sure that you have `mavenCentral()` in your Gradle scripts.
The instruction on how to add it If your project doesn't have `dependencyResolutionManagement` in your `settings.gradle`, add the following to your top-level `build.gradle` at the end of repositories: ```groovy showLineNumbers title="top-level build.gradle" allprojects { repositories { ... mavenCentral() } } ``` Otherwise, add the following to your `settings.gradle` in `repositories` of `dependencyResolutionManagement` section: ```groovy showLineNumbers title="settings.gradle" dependencyResolutionManagement { ... repositories { ... mavenCentral() } } ```
### Configure Proguard You should add `-keep class com.adapty.** { *; }` to your Proguard configuration. ### Configure Adapty SDK Add the following to your `Application` class: ```kotlin showLineNumbers override fun onCreate() { super.onCreate() Adapty.activate( applicationContext, AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withObserverMode(false) //default false .withCustomerUserId(customerUserId) //default null .withIpAddressCollectionDisabled(false) //default false .build() ) //OR //the method is deprecated since Adapty SDK v2.10.5 Adapty.activate(applicationContext, "PUBLIC_SDK_KEY", observerMode = false, customerUserId = "YOUR_USER_ID") } ``` ```java showLineNumbers @Override public void onCreate() { super.onCreate(); Adapty.activate( applicationContext, new AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withObserverMode(false) //default false .withCustomerUserId(customerUserId) //default null .withIpAddressCollectionDisabled(false) //default false .build() ); //OR //the method is deprecated since Adapty SDK v2.10.5 Adapty.activate(getApplicationContext(), "PUBLIC_SDK_KEY", false, "YOUR_USER_ID"); } ``` Configurational options: | Parameter | Presence | Description | | --------------------------- | -------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | apiKey | required | The key you can find in the **Public SDK key** field of your app settings in Adapty: [**App settings**-> **General** tab -> **API keys** subsection](https://app.adapty.io/settings/general). Make sure you use the **Public SDK key** for Adapty initialization, the **Secret key** should be used for [server-side API](getting-started-with-server-side-api) only. | | observerMode | optional | A boolean value that controls [Observer mode](observer-vs-full-mode). Turn it on if you handle purchases and subscription status yourself and use Adapty for sending subscription events and analytics. The default value is `false`. 🚧 When running in Observer mode, Adapty SDK won't close any transactions, so make sure you're handling it. | | customerUserId | optional | An identifier of the user in your system. We send it in subscription and analytical events, to attribute events to the right profile. You can also find customers by `customerUserId` in the [**Profiles and Segments**](https://app.adapty.io/profiles/users) menu. If you don't have a user ID at the time of Adapty initialization, you can set it later using `.identify()` method. Read more in the [Identifying users](android-identifying-users) section. | | IpAddressCollectionDisabled | optional | A boolean parameter. Set to `true` to disable the collection of the user IP address. The default value is `false`. Parameter works with `AdaptyConfig.Builder` only. | :::note **SDK keys** are unique for every app, so if you have multiple apps make sure you choose the right one. ::: --- # File: android-get-legacy-pb-paywalls --- --- title: "Fetch legacy Paywall Builder paywalls in Android SDK" description: "Retrieve legacy PB paywalls in your Android app with Adapty SDK." --- After [you designed the visual part for your paywall](adapty-paywall-builder-legacy) with Paywall Builder in the Adapty Dashboard, you can display it in your Android app. The first step in this process is to get the paywall associated with the placement and its view configuration as described below. :::warning This guide is for **legacy Paywall Builder paywalls** only which require SDK v2.x or earlier. The process for fetching paywalls differs for paywalls designed with different versions of Paywall Builder and remote config paywalls. - For fetching **New Paywall Builder paywalls**, check out [Fetch new Paywall Builder paywalls and their configuration](android-get-pb-paywalls). - For fetching **Remote config paywalls**, see [Fetch paywalls and products for remote config paywalls](fetch-paywalls-and-products-android). :::
Before you start displaying paywalls in your Android app (click to expand) 1. [Create your products](create-product) in the Adapty Dashboard. 2. [Create a paywall and incorporate the products into it](create-paywall) in the Adapty Dashboard. 3. [Create placements and incorporate your paywall into it](create-placement) in the Adapty Dashboard. 4. [Install Adapty SDK and AdaptyUI DSK](sdk-installation-android) in your Android app.
## Fetch paywall designed with Paywall Builder If you've [designed a paywall using the Paywall Builder](adapty-paywall-builder-legacy), you don't need to worry about rendering it in your Android app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. Nevertheless, you need to get its ID via the placement, its view configuration, and then present it in your Android app. To ensure optimal performance, it's crucial to retrieve the paywall and its [view configuration](#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) as early as possible, allowing sufficient time for images to download before presenting them to the user. To get a paywall, use the `getPaywall` method: ```kotlin showLineNumbers ... Adapty.getPaywall("YOUR_PLACEMENT_ID", locale = "en", loadTimeout = 10.seconds) { result -> when (result) { is AdaptyResult.Success -> { val paywall = result.value // the requested paywall } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` For Java: ```java showLineNumbers ... Adapty.getPaywall("YOUR_PLACEMENT_ID", "en", TimeInterval.seconds(10), result -> { if (result instanceof AdaptyResult.Success) { AdaptyPaywall paywall = ((AdaptyResult.Success) result).getValue(); // the requested paywall } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the [paywall localization](add-paywall-locale-in-adapty-paywall-builder). This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.

| | **loadTimeout** | default: 5 sec |

This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.

Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.

For Android: You can create `TimeInterval` with extension functions (like `5.seconds`, where `.seconds` is from `import com.adapty.utils.seconds`), or `TimeInterval.seconds(5)`. To set no limitation, use `TimeInterval.INFINITE`.

| **Don't hardcode product IDs.** The only ID you should hardcode is the placement ID. Paywalls are configured remotely, so the number of products and available offers can change at any time. Your app must handle these changes dynamically—if a paywall returns two products today and three tomorrow, display all of them without code changes. Response parameters: | Parameter | Description | | :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](https://android.adapty.io/adapty/com.adapty.models/-adapty-paywall/) object with a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch the view configuration of paywall designed using Paywall Builder After fetching the paywall, check if it includes a `viewConfiguration`, which indicates that it was created using Paywall Builder. This will guide you on how to display the paywall. If the `viewConfiguration` is present, treat it as a Paywall Builder paywall; if not, [handle it as a remote config paywall](present-remote-config-paywalls-android). Use the `getViewConfiguration` method to load the view configuration. ```kotlin showLineNumbers if (!paywall.hasViewConfiguration) { // use your custom logic return } AdaptyUI.getViewConfiguration(paywall) { result -> when(result) { is AdaptyResult.Success -> { val viewConfiguration = result.value // use loaded configuration } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` --- # File: android-present-paywalls-legacy --- --- title: "Present legacy Paywall Builder paywalls in Android SDK" description: "Present paywalls in Android (Legacy) and manage subscriptions effectively." --- If you've customized a paywall using the Paywall Builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. :::warning This guide covers the process for **legacy Paywall Builder paywalls** only which requires SDK v2.x or earlier. The process for presenting paywalls differs for paywalls designed with different versions of Paywall Builde, remote config paywalls, and [Observer mode](observer-vs-full-mode). - For presenting **New Paywall Builder paywalls**, check out [Android - Present new Paywall Builder paywalls](android-present-paywalls). - For presenting **Remote config paywalls**, see [Render paywall designed by remote config](present-remote-config-paywalls). - For presenting **Observer mode paywalls**, see [Android - Present Paywall Builder paywalls in Observer mode](android-present-paywall-builder-paywalls-in-observer-mode) ::: In order to display the visual paywall on the device screen, you must first configure it. To do this, call the method `AdaptyUI.getPaywallView()` or create the `AdaptyPaywallView` directly: ```kotlin showLineNumbers val paywallView = AdaptyUI.getPaywallView( activity, viewConfiguration, products, AdaptyPaywallInsets.of(topInset, bottomInset), eventListener, personalizedOfferResolver, tagResolver, ) //======= OR ======= val paywallView = AdaptyPaywallView(activity) // or retrieve it from xml ... with(paywallView) { setEventListener(eventListener) setObserverModeHandler(observerModeHandler) showPaywall( viewConfiguration, products, AdaptyPaywallInsets.of(topInset, bottomInset), personalizedOfferResolver, tagResolver, ) } ``` ```java showLineNumbers AdaptyPaywallView paywallView = AdaptyUI.getPaywallView( activity, viewConfiguration, products, AdaptyPaywallInsets.of(topInset, bottomInset), eventListener, personalizedOfferResolver, tagResolver ); //======= OR ======= AdaptyPaywallView paywallView = new AdaptyPaywallView(activity); //add to the view hierarchy if needed, or you receive it from xml ... paywallView.setEventListener(eventListener); paywallView.showPaywall(viewConfiguration, products, AdaptyPaywallInsets.of(topInset, bottomInset), personalizedOfferResolver); ``` ```xml showLineNumbers ``` After the view has been successfully created, you can add it to the view hierarchy and display it on the screen of the device. If you get `AdaptyPaywallView` _not_ by calling `AdaptyUI.getPaywallView()`, you will also need to call `.setEventListener()` and `.showPaywall()` methods. Request parameters: | Parameter | Presence | Description | | :---------------------------- | :------- |:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Paywall** | required | Specify an `AdaptyPaywall` object, for which you are trying to get a screen representation. | | **Products** | optional | Provide an array of `AdaptyPaywallProduct `to optimize the display timing of products on the screen. If `null` is passed, AdaptyUI will automatically fetch the required products. | | **ViewConfiguration** | required | Supply an `AdaptyViewConfiguration` object containing visual details of the paywall. Use the `Adapty.getViewConfiguration(paywall)` method to load it. Refer to [Fetch the visual configuration of paywall](android-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) topic for more details. | | **Insets** | required | Define an `AdaptyPaywallInsets` object containing information about the area overlapped by system bars, creating vertical margins for content. If neither the status bar nor the navigation bar overlaps the `AdaptyPaywallView`, pass `AdaptyPaywallInsets.NONE`. For fullscreen mode where system bars overlap part of your UI, obtain insets as shown under the table. | | **EventListener** | optional | Provide an `AdaptyUiEventListener` to observe paywall events. Extending AdaptyUiDefaultEventListener is recommended for ease of use. Refer to [Handling paywall events](android-handling-events) topic for more details. | | **PersonalizedOfferResolver** | optional | To indicate personalized pricing ([read more](https://developer.android.com/google/play/billing/integrate#personalized-price) ), implement `AdaptyUiPersonalizedOfferResolver` and pass your own logic that maps `AdaptyPaywallProduct` to true if the product's price is personalized, otherwise false. | | **TagResolver** | optional | Use `AdaptyUiTagResolver` to resolve custom tags within the paywall text. This resolver takes a tag parameter and resolves it to a corresponding string. Refer to [Custom tags in Paywall Builder](custom-tags-in-paywall-builder) topic for more details. | For fullscreen mode where system bars overlap part of your UI, obtain insets in the following way: ```kotlin showLineNumbers //create extension function fun View.onReceiveSystemBarsInsets(action: (insets: Insets) -> Unit) { ViewCompat.setOnApplyWindowInsetsListener(this) { _, insets -> val systemBarInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) ViewCompat.setOnApplyWindowInsetsListener(this, null) action(systemBarInsets) insets } } //and then use it with the view paywallView.onReceiveSystemBarsInsets { insets -> val paywallInsets = AdaptyPaywallInsets.of(insets.top, insets.bottom) paywallView.showPaywall(paywall, products, viewConfig, paywallInsets, productTitleResolver) } ``` ```java showLineNumbers ... ViewCompat.setOnApplyWindowInsetsListener(paywallView, (view, insets) -> { Insets systemBarInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()); ViewCompat.setOnApplyWindowInsetsListener(paywallView, null); AdaptyPaywallInsets paywallInsets = AdaptyPaywallInsets.of(systemBarInsets.top, systemBarInsets.bottom); paywallView.showPaywall(paywall, products, viewConfiguration, paywallInsets, productTitleResolver); return insets; }); ``` Returns: | Object | Description | | :------------------ | :------------------------------------------------- | | `AdaptyPaywallView` | object, representing the requested paywall screen. | **Next step:** - [Handle paywall events](android-handling-events-legacy) --- # File: android-handling-events-legacy --- --- title: "Handle paywall events in legacy Android SDK" description: "Handle subscription events in Android (Legacy) with Adapty SDK." --- Paywalls configured with the [Paywall Builder](adapty-paywall-builder-legacy) don't need extra code to make and restore purchases. However, they generate some events that your app can respond to. Those events include button presses (close buttons, URLs, product selections, and so on) as well as notifications on purchase-related actions taken on the paywall. Learn how to respond to these events below. :::warning This guide is for **legacy Paywall Builder paywalls** only which require Adapty SDK up to v2.x. For presenting paywalls in Adapty SDK v3.0 or later designed with the new Paywall Builder, see [Android - Handle paywall events designed with the new Paywall Builder](android-handling-events). ::: If you need to control or monitor the processes that take place on the purchase screen, implement the `AdaptyUiEventListener` methods. If you would like to leave the default behavior in some cases, you can extend `AdaptyUiDefaultEventListener` and override only those methods you want to change. Below are the defaults from `AdaptyUiDefaultEventListener`. ### User-generated events #### Actions If a user has performed some action (`Close`, `OpenURL` or `Custom`, this method will be invoked: ```kotlin showLineNumbers title="Kotlin" override fun onActionPerformed(action: AdaptyUI.Action, view: AdaptyPaywallView) { when (action) { AdaptyUI.Action.Close -> (view.context as? Activity)?.onBackPressed() is AdaptyUI.Action.OpenUrl -> //launching intent to open url is AdaptyUI.Action.Custom -> //no default action } } ``` The following action types are supported: - `Close` - `OpenUrl(url)` - `Custom(id)` Note that at the very least you need to implement the reactions to both `Close` and `OpenURL`. For example, if a user taps the close button, the action `Close` will occur and you are supposed to dismiss the paywall. :::warning This method is _not_ invoked when user taps the system back button instead of the close icon on the screen. ::: > 💡 Login Action > > If you have configured Login Action in the dashboard, you should implement reaction for custom action with id `"login"` #### Product selection If a product is selected for purchase (by a user or by the system), this method will be invoked: ```kotlin showLineNumbers title="Kotlin" public override fun onProductSelected( product: AdaptyPaywallProduct, view: AdaptyPaywallView, ) {} ``` #### Started purchase If a user initiates the purchase process, this method will be invoked: ```kotlin showLineNumbers title="Kotlin" public override fun onPurchaseStarted( product: AdaptyPaywallProduct, view: AdaptyPaywallView, ) {} ``` The method will not be invoked in Observer mode. Refer to the [Android - Present Paywall Builder paywalls in Observer mode](android-present-paywall-builder-paywalls-in-observer-mode) topic for details. #### Canceled purchase If a user initiates the purchase process but manually interrupts it afterward, the method below will be invoked. This event occurs when the `Adapty.makePurchase()` function completes with the `USER_CANCELED` error: ```kotlin showLineNumbers title="Kotlin" public override fun onPurchaseCanceled( product: AdaptyPaywallProduct, view: AdaptyPaywallView, ) {} ``` The method will not be invoked in Observer mode. Refer to the [Android - Present Paywall Builder paywalls in Observer mode](android-present-paywall-builder-paywalls-in-observer-mode) topic for details. #### Successful purchase If `Adapty.makePurchase()` succeeds, this method will be invoked: ```kotlin showLineNumbers title="Kotlin" public override fun onPurchaseSuccess( profile: AdaptyProfile?, product: AdaptyPaywallProduct, view: AdaptyPaywallView, ) { (view.context as? Activity)?.onBackPressed() } ``` We recommend dismissing the screen in that case. The method will not be invoked in Observer mode. Refer to the [Android - Present Paywall Builder paywalls in Observer mode](android-present-paywall-builder-paywalls-in-observer-mode) topic for details. #### Failed purchase If `Adapty.makePurchase()` fails, this method will be invoked: ```kotlin showLineNumbers title="Kotlin" public override fun onPurchaseFailure( error: AdaptyError, product: AdaptyPaywallProduct, view: AdaptyPaywallView, ) {} ``` The method will not be invoked in Observer mode. Refer to the [Android - Present Paywall Builder paywalls in Observer mode](android-present-paywall-builder-paywalls-in-observer-mode) topic for details. #### Successful restore If `Adapty.restorePurchases()` succeeds, this method will be invoked: ```kotlin showLineNumbers title="Kotlin" public override fun onRestoreSuccess( profile: AdaptyProfile, view: AdaptyPaywallView, ) {} ``` We recommend dismissing the screen if the user has the required `accessLevel`. Refer to the [Subscription status](android-listen-subscription-changes.md) topic to learn how to check it. #### Failed restore If `Adapty.restorePurchases()` fails, this method will be invoked: ```kotlin showLineNumbers title="Kotlin" public override fun onRestoreFailure( error: AdaptyError, view: AdaptyPaywallView, ) {} ``` ### Data fetching and rendering #### Product loading errors If you don't pass the products during the initialization, AdaptyUI will retrieve the necessary objects from the server by itself. If this operation fails, AdaptyUI will report the error by invoking this method: ```kotlin showLineNumbers title="Kotlin" public override fun onLoadingProductsFailure( error: AdaptyError, view: AdaptyPaywallView, ): Boolean = false ``` If you return `true`, AdaptyUI will repeat the request in 2 seconds. #### Rendering errors If an error occurs during the interface rendering, it will be reported by calling this method: ```kotlin showLineNumbers title="Kotlin" public override fun onRenderingError( error: AdaptyError, view: AdaptyPaywallView, ) {} ``` In a normal situation, such errors should not occur, so if you come across one, please let us know. --- # End of Documentation _Generated on: 2026-03-05T16:27:47.968Z_ _Successfully processed: 41/41 files_ # API - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Generated on: 2026-03-05T16:27:47.971Z Total files: 14 --- # File: getting-started-with-server-side-api --- --- title: "Server-side API" description: "Get started with Adapty's server-side API for subscription management." --- With the API, you can: 1. Check a user's subscription status. 2. Activate a user's subscription with an access level. 3. Retrieve user attributes. 4. Set user attributes. 5. Get and update paywall configurations.

:::note To track subscription events, use [Webhook](webhook) integration in Adapty or integrate directly with your existing service. ::: ## Case 1: Sync subscribers between web and mobile If you use web payment providers like Stripe, ChargeBee, or others, you can sync your subscribers easily. Here's how: 1. [iOS](identifying-users), [Android](android-identifying-users), [React Native](react-native-identifying-users), [Flutter](flutter-identifying-users), and [Unity](unity-identifying-users). 2. [Check their subscription status](api-adapty/operations/getProfile) using the API. 3. If a user is on a freemium plan, display a paywall on your website. 4. After a successful payment, [update the subscription status](api-adapty/operations/setTransaction) in Adapty via API. 5. Your subscribers will automatically stay in sync with your mobile app. ## Case 2: Grant a subscription :::note Due to security reasons, you can't grant a subscription via mobile SDK. ::: If you're selling through your own online store, Amazon Appstore, Microsoft Store, or any other platform besides Google Play and App Store, you'll need to sync those transactions with Adapty to provide access and track the transaction in analytics. 1. [iOS](identifying-users), [Android](android-identifying-users), [React Native](react-native-identifying-users), [Flutter](flutter-identifying-users), and [Unity](unity-identifying-users). 2. [Set up a custom store for your products in the Adapty Dashboard](custom-store). 3. Sync the transaction to Adapty using the [Set transaction](api-adapty/operations/setTransaction) API request. ## Case 3: Grant an access level Let's say you're running a promotion offering a 7-day free trial and you want the experience to be consistent across platforms. To sync this with the mobile app: 1. [iOS](identifying-users), [Android](android-identifying-users), [React Native](react-native-identifying-users), [Flutter](flutter-identifying-users), and [Unity](unity-identifying-users). 2. Use the API to [grant premium access](api-adapty/operations/grantAccessLevel) for 7 days. After the 7 days, users who don't subscribe will be downgraded to the free tier. ## Case 4: Sync users' properties and custom attributes If you have custom attributes for your users—such as the number of words learned in a language learning app—you can sync them as well. 1. [iOS](identifying-users), [Android](android-identifying-users), [React Native](react-native-identifying-users), [Flutter](flutter-identifying-users), and [Unity](unity-identifying-users). 2. [Update the attribute](api-adapty/operations/updateProfile) via API or SDK. These custom attributes can be used to create segments and run A/B tests. ## Case 5: Manage paywall configurations You can [update remote configs in paywalls](api-adapty/operations/updatePaywall.md) to dynamically adjust your paywall appearance and behavior without redeploying your app. --- **What's next:** - Proceed with [authorization for server-side API](ss-authorization) - Requests: - [Get profile](api-adapty/operations/getProfile) - [Create profile](api-adapty/operations/createProfile) - [Update profile](api-adapty/operations/updateProfile) - [Delete profile](api-adapty/operations/deleteProfile) - [Grant access level](api-adapty/operations/grantAccessLevel) - [Revoke access level](api-adapty/operations/revokeAccessLevel) - [Set transaction](api-adapty/operations/setTransaction) - [Validate purchase, provide access level to customer, and import their transaction history](api-adapty/operations/validateStripePurchase) - [Add integration identifiers](api-adapty/operations/setIntegrationIdentifiers) - [Get paywall](api-adapty/operations/getPaywall) - [List paywalls](api-adapty/operations/listPaywalls) - [Update paywall](api-adapty/operations/updatePaywall) --- # File: ss-authorization --- --- title: "Server-side API Authorization and request format" description: "" --- ## Authorization API requests must be authenticated with either your secret or your public API key as an Authorization header. You can find them in the [**App Settings**](https://app.adapty.io/settings/general). The format of the value is `Api-Key {your-secret-api-key}`, for example, `Api-Key secret_live_...`. ## Request format **Headers** The server-side API requests require specific headers and a JSON body. Use the details below to structure your requests. | **Header** | **Description** | | --------------------------- | ------------------------------------------------------------ | | **adapty-profile-id** |

The user’s Adapty profile ID. Visible in the **Adapty ID** field in the [Adapty Dashboard -> **Profiles**](https://app.adapty.io/profiles/users) -> specific profile page.

Interchangeable with **adapty-customer-user-id**, use any of them.

| | **adapty-customer-user-id** |

The user's ID in your system. Visible in the **Customer user ID** field in the [Adapty Dashboard -> **Profiles**](https://app.adapty.io/profiles/users) -> specific profile page.

Interchangeable with **adapty-profile-id**, use any of them.

⚠️ Works only if you [iOS](identifying-users), [Android](android-identifying-users), [React Native](react-native-identifying-users), [Flutter](flutter-identifying-users), and [Unity](unity-identifying-users) in your app code using the Adapty SDK.

| | **adapty-platform** | (optional) Specify the platform of the device on which the app is installed. We recommend setting this parameter in the [Create profile](api-adapty/operations/createProfile) and [Update profile](api-adapty/operations/updateProfile) requests when modifying the [Installation Meta](server-side-api-objects#installation-meta) object, as it depends on the device the user is using, and a single user may have multiple devices. Possible values: `iOS`, `macOS`, `iPadOS`, `visionOS`, `Android`, or `web`. | | **Content-Type** | Set to `application/json` for the API to process the request. | **Body** The API expects a JSON-formatted body with the necessary data for the request. ## Rate limits To avoid throttling, ensure that the number of requests (per app) stays below 40,000 per minute. If this limit is exceeded, the system may slow down or temporarily block further requests to maintain optimal performance for all users. --- **What's next: requests:** - [Get profile](api-adapty/operations/getProfile) - [Create profile](api-adapty/operations/createProfile) - [Update profile](api-adapty/operations/updateProfile) - [Delete profile](api-adapty/operations/deleteProfile) - [Grant access level](api-adapty/operations/grantAccessLevel) - [Revoke access level](api-adapty/operations/revokeAccessLevel) - [Set transaction](api-adapty/operations/setTransaction) - [Validate purchase, provide access level to customer, and import their transaction history](api-adapty/operations/validateStripePurchase) - [Get paywall](api-adapty/operations/getPaywall) - [List paywalls](api-adapty/operations/listPaywalls) - [Update paywall](api-adapty/operations/updatePaywall) --- # File: server-side-api-specs --- --- title: "Server-side API requests" description: "Explore Adapty’s server-side API specifications for advanced integration." --- Adapty's server-side API empowers you to programmatically access and manage your subscription data, enabling seamless integration with your existing services and infrastructure. Whether you're syncing data across platforms, granting access levels, or validating purchases in Stripe, this API provides the tools to keep your systems in sync and your users engaged. ## Postman collection and environment To simplify using our server-side API, we've prepared a Postman collection and an environment file you can download and import into Postman. - **Request Collection**: Includes all requests available in the Adapty server-side API. Note that it uses variables that you can define in the environment. - **Environment**: Contains a list of variables where you can define values once. We've prepared a unified environment for the server-side API, web API, and analytics export API to make things easier for you. After making this environment active, Postman will automatically substitute the defined variable values in your requests. :::tip [Download the collection and environment](https://raw.githubusercontent.com/adaptyteam/adapty-docs/refs/heads/main/Downloads/Adapty_server_side_API_postman_collection.zip) ::: For info on how to import a collection and environment to Postman, please refer to the [Postman documentation](https://learning.postman.com/docs/getting-started/importing-and-exporting/importing-data/). ### Variables used We've created a unified environment for the server-side API, web API, and analytics export API to simplify your workflow. Below are the variables specific to the server-side API: | Variable | Description | Example Value | | ----------------------- | ------------------------------------------------------------ | ------------------------------------------------------- | | secret_api_key | You can find it in the **Secret key** field in the [**App settings**](https://app.adapty.io/settings/general). | `secret_live_Pj1P1xzM.2CvSvE1IalQRFjsWy6csBVNpH33atnod` | | adapty-customer-user-id | The user ID used in your system. In the Adapty Dashboard, you can find it in the **Customer user ID** field of the Profile. | `john.doe@example.com` | | adapty-profile-id | The user ID assigned in Adapty. In the Adapty Dashboard, you can find it in the **Adapty ID** field of the Profile. | `3286abd3-48b0-4e9c-a5f6-ac0a006333a6` | | Adapty-platform | The platform used by the user for your app. Possible values: `iOS`, `macOS`, `iPadOS`, `visionOS`, `Android`, `web`. | `iOS` | | stripe_token | Token of a Stripe object representing a unique purchase, such as a Subscription (`sub_XXX`) or Payment Intent (`pi_XXX`). | `sub_1JY8xLLy6P12345a` | **What's next: Requests:** - [Get profile](api-adapty/operations/getProfile) - [Create profile](api-adapty/operations/createProfile) - [Update profile](api-adapty/operations/updateProfile) - [Delete profile](api-adapty/operations/deleteProfile) - [Grant access level](api-adapty/operations/grantAccessLevel) - [Revoke access level](api-adapty/operations/revokeAccessLevel) - [Set transaction](api-adapty/operations/setTransaction) - [Validate purchase, provide access level to customer, and import their transaction history](api-adapty/operations/validateStripePurchase) - [Add integration identifiers](api-adapty/operations/setIntegrationIdentifiers) - [Get paywall](api-adapty/operations/getPaywall) - [List paywalls](api-adapty/operations/listPaywalls) - [Update paywall](api-adapty/operations/updatePaywall) --- # File: api-guides --- --- title: "API guides" description: "Learn how to perform specific tasks using the server-side API." --- In this section, you can find guides that cover different use cases and help you perform specific tasks using the server-side API and the Adapty SDK. --- # File: sync-subscribers-from-web --- --- title: "Sync purchases between web and mobile" description: "Sync subscribers on web and mobile." --- If your users can purchase a product on your **website**, you can keep their access levels automatically synced with your **mobile app**. In this guide, you will learn how to do it using the Adapty API and SDK. #### Sample use case Let's say, in your app, sers can sign up for a freemium plan on both mobile and web. You allow them to upgrade to a Premium plan on your website via Stripe or Chargebee. Once a user subscribes on the web, you want them to immediately get Premium access in the mobile app — without waiting or re-logging. That’s what Adapty helps you automate. ## Step 1. Identify users Adapty uses `customer_user_id` to identify users across platforms. You should create this ID once and pass it to both your mobile SDK and web backend. ### Sign up from web When your users sign up on your website, you need to create a profile for them in Adapty using the server-side API. See the method reference [here](api-adapty/operations/createProfile). ```curl curl --request POST \ --url https://api.adapty.io/api/v2/server-side-api/profile/ \ --header 'Accept: application/json' \ --header 'Authorization: Api-Key YOUR_SECRET_API_KEY' \ --header 'Content-Type: application/json' \ --header 'adapty-customer-user-id: YOUR_CUSTOMER_USER_ID' ``` ### Sign up from app When your users first sign up from the app, you can pass their customer user ID during the SDK activation, or if you have activated the Adapty SDK before the signup stage, use the `identify` method to create a new profile and assign it a customer user ID. :::important If you identify new users after the SDK activation, first, the SDK will create an anonymous profile, as it can't work without any profile at all. Next, when you identify the user and assign them a new customer user ID, a new profile will be created. This behavior is completely normal, and it won't affect the analytics accuracy. Read more [here](ios-quickstart-identify.md ). ::: ```swift showLineNumbers do { try await Adapty.identify("YOUR_USER_ID") // Unique for each user } catch { // handle the error } ``` ```swift showLineNumbers // User IDs must be unique for each user Adapty.identify("YOUR_USER_ID") { error in if let error { // handle the error } } ``` ```kotlin showLineNumbers Adapty.identify("YOUR_USER_ID") { error -> // Unique for each user if (error == null) { // successful identify } } ``` ```java showLineNumbers // User IDs must be unique for each user Adapty.identify("YOUR_USER_ID", error -> { if (error == null) { // successful identify } }); ``` ```typescript showLineNumbers try { await adapty.identify("YOUR_USER_ID"); // Unique for each user // successfully identified } catch (error) { // handle the error } ``` ```dart showLineNumbers try { await Adapty().identify(customerUserId); // Unique for each user } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ```csharp showLineNumbers Adapty.Identify("YOUR_USER_ID", (error) => { // Unique for each user if(error == null) { // successful identify } }); ``` ```kotlin showLineNumbers Adapty.identify("YOUR_USER_ID") // Unique for each user .onSuccess { // successful identify } .onError { error -> // handle the error } ``` ```typescript showLineNumbers try { await adapty.identify({ customerUserId: "YOUR_USER_ID" }); // successfully identified } catch (error) { // handle the error } ``` ## Step 2. Check subscription status via API When a user logs in on your website, fetch their Adapty profile using the API. If the user doesn’t have an active subscription, you can display a paywall. See the method reference [here](api-adapty/operations/getProfile). ```curl curl --request GET \ --url https://api.adapty.io/api/v2/server-side-api/profile/ \ --header 'Accept: application/json' \ --header 'Authorization: Api-Key YOUR_SECRET_API_KEY' \ --header 'adapty-customer-user-id: YOUR_USER_ID' \ ``` ## Step 3. Display a paywall on your website On your website, show a paywall for freemium users. You can use any payment provider (Stripe, Chargebee, LemonSqueezy, etc.). ## Step 4. Update subscription status in Adapty After the payment is completed on your website, call Adapty API to update the user’s access level according to the product they bought. See the method reference [here](api-adapty/operations/grantAccessLevel). ```curl curl --request POST \ --url https://api.adapty.io/api/v2/server-side-api/purchase/profile/grant/access-level/ \ --header 'Accept: application/json' \ --header 'Authorization: Api-Key YOUR_SECRET_API_KEY' \ --header 'Content-Type: application/json' \ --header 'adapty-customer-user-id: YOUR_USER_ID' \ --data '{ "access_level_id": "YOUR_ACCESS_LEVEL" }' ``` ## Step 5. Sync status in the app When the user opens your mobile app, pull the updated profile and unlock paid features. You need to either get their profile or sync it automatically. Then, get the access level from it. Below, you see how to get the profile and check its status. For more details, go [here](ios-check-subscription-status.md). ```swift showLineNumbers do { let profile = try await Adapty.getProfile() if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // grant access to premium features } } catch { // handle the error } ``` ```swift showLineNumbers Adapty.getProfile { result in if let profile = try? result.get() { // check the access if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // grant access to premium features } } } ``` ```kotlin showLineNumbers Adapty.getProfile { result -> when (result) { is AdaptyResult.Success -> { val profile = result.value // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive == true) { // grant access to premium features } } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers Adapty.getProfile(result -> { if (result instanceof AdaptyResult.Success) { AdaptyProfile profile = ((AdaptyResult.Success) result).getValue(); // check the access if (profile.getAccessLevels().get("YOUR_ACCESS_LEVEL") != null && profile.getAccessLevels().get("YOUR_ACCESS_LEVEL").getIsActive()) { // grant access to premium features } } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` ```typescript showLineNumbers try { const profile = await adapty.getProfile(); // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive) { // grant access to premium features } } catch (error) { // handle the error } ``` ```dart showLineNumbers try { final profile = await Adapty().getProfile(); // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false) { // grant access to premium features } } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ```csharp showLineNumbers Adapty.GetProfile((profile, error) => { if (error != null) { // handle the error return; } // check the access if (profile.AccessLevels["YOUR_ACCESS_LEVEL"]?.IsActive ?? false) { // grant access to premium features } }); ``` ```kotlin showLineNumbers Adapty.getProfile() .onSuccess { profile -> // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive == true) { // grant access to premium features } } .onError { error -> // handle the error } ``` ```typescript showLineNumbers try { const profile = await adapty.getProfile(); // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive) { // grant access to premium features } } catch (error) { // handle the error } ``` --- # File: sync-purchases-from-custom-stores --- --- title: "Sync transactions from custom stores" description: "Sync transactions from custom stores to Adapty to provide access and track revenue." --- If you're selling subscriptions or in-app purchases through **custom stores** like Amazon Appstore, Microsoft Store, or your own payment platform, you can sync those transactions with Adapty to automatically manage access levels and track revenue in your analytics. In this guide, you'll learn how to connect custom store purchases with Adapty using the SDK and API. #### Sample use case Let's say you're distributing your app on Amazon Appstore, or you've built your own web store for direct purchases. When a user completes a purchase through these platforms, you want to: - Automatically grant them access to premium features in your mobile app - Track the transaction in Adapty analytics alongside your App Store and Google Play revenue - Trigger integrations and webhooks just like any other subscription That's what this integration helps you achieve. ## Step 1. Identify users Adapty uses `customer_user_id` to identify users across platforms. You need to create this ID once and pass it to both your mobile SDK and web backend. When your users first sign up from the app, you can pass their customer user ID during the SDK activation, or if you have activated the Adapty SDK before the signup stage, use the `identify` method to create a new profile and assign it a customer user ID. :::important If you identify new users after SDK activation, the SDK will first create an anonymous profile (it can't work without one). When you call `identify` with a customer user ID, a new profile will be created. This behavior is normal and won't affect analytics accuracy. Read more [here](ios-quickstart-identify.md). ::: ```swift showLineNumbers do { try await Adapty.identify("YOUR_USER_ID") // Unique for each user } catch { // handle the error } ``` ```swift showLineNumbers // User IDs must be unique for each user Adapty.identify("YOUR_USER_ID") { error in if let error { // handle the error } } ``` ```kotlin showLineNumbers Adapty.identify("YOUR_USER_ID") { error -> // Unique for each user if (error == null) { // successful identify } } ``` ```java showLineNumbers // User IDs must be unique for each user Adapty.identify("YOUR_USER_ID", error -> { if (error == null) { // successful identify } }); ``` ```typescript showLineNumbers try { await adapty.identify("YOUR_USER_ID"); // Unique for each user // successfully identified } catch (error) { // handle the error } ``` ```dart showLineNumbers try { await Adapty().identify(customerUserId); // Unique for each user } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ```csharp showLineNumbers Adapty.Identify("YOUR_USER_ID", (error) => { // Unique for each user if(error == null) { // successful identify } }); ``` ```kotlin showLineNumbers Adapty.identify("YOUR_USER_ID") // Unique for each user .onSuccess { // successful identify } .onError { error -> // handle the error } ``` ```typescript showLineNumbers try { await adapty.identify({ customerUserId: "YOUR_USER_ID" }); // successfully identified } catch (error) { // handle the error } ``` ## Step 2. Create products in a custom store in Adapty Dashboard For Adapty to match custom store transactions with your products, you need to add products and and set up the custom store details for them. 1. Go to [**Products**](https://app.adapty.io/settings/general) from the left menu in the Adapty Dashboard and click **Create product**. Or, click an existing product to edit it. 2. Ensure you have selected an [access level](access-level.md) you want to grant users purchasing the product. 3. Click **+** and select **Add a custom store**. 4. Click **Create new custom store**. 5. Give your store a name (e.g., "Amazon Appstore", "Microsoft Store", or "Web Store") and ID. Click **Create custom store**. 6. Then, click **Save changes** to link the product to the custom store. 7. Enter **Store product ID** for the product, so you map it with some product in that store. Then, click **Save**. ## Step 3. Sync transactions via API When a purchase is completed in your custom store, you need to sync it to Adapty using the server-side API. This API call will: - Record the transaction in Adapty - Grant the corresponding access level to the user - Trigger any integrations and webhooks you've configured - Make the transaction appear in your analytics See the full method reference [here](api-adapty/operations/setTransaction). ```curl curl --request POST \ --url https://api.adapty.io/api/v2/server-side-api/purchase/set/transaction/ \ --header 'Accept: application/json' \ --header 'Authorization: Api-Key YOUR_SECRET_API_KEY' \ --header 'Content-Type: application/json' \ --header 'adapty-customer-user-id: YOUR_CUSTOMER_USER_ID' \ --data '{ "purchase_type": "PRODUCT_PERIOD", "store": "YOUR_CUSTOM_STORE", "environment": "production", "store_product_id": "YOUR_STORE_PRODUCT_ID", "store_transaction_id": "STORE_TRANSACTION_ID", "store_original_transaction_id": "ORIGINAL_TRANSACTION_ID", "price": { "country": "COUNTRY_CODE", "currency": "CURRENCY_CODE", "value": "YOUR_PRICE" }, "purchased_at": "2024-01-15T10:30:00Z" }' ``` :::important Important parameters: - **store**: The ID of your custom store from Step 2 - **store_product_id**: Store product ID from Step 2 - **store_transaction_id**: A unique identifier for this transaction - **purchased_at**: ISO 8601 timestamp when the purchase occurred - **price**: The amount paid by the user ::: ## Step 4. Verify access in the app Once the transaction is synced, the user's profile will be automatically updated with the new access level. When the user opens your mobile app, fetch their profile to check their subscription status and unlock premium features. ```swift showLineNumbers do { let profile = try await Adapty.getProfile() if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // grant access to premium features } } catch { // handle the error } ``` ```swift showLineNumbers Adapty.getProfile { result in if let profile = try? result.get() { // check the access if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // grant access to premium features } } } ``` ```kotlin showLineNumbers Adapty.getProfile { result -> when (result) { is AdaptyResult.Success -> { val profile = result.value // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive == true) { // grant access to premium features } } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers Adapty.getProfile(result -> { if (result instanceof AdaptyResult.Success) { AdaptyProfile profile = ((AdaptyResult.Success) result).getValue(); // check the access if (profile.getAccessLevels().get("YOUR_ACCESS_LEVEL") != null && profile.getAccessLevels().get("YOUR_ACCESS_LEVEL").getIsActive()) { // grant access to premium features } } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` ```typescript showLineNumbers try { const profile = await adapty.getProfile(); // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive) { // grant access to premium features } } catch (error) { // handle the error } ``` ```dart showLineNumbers try { final profile = await Adapty().getProfile(); // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false) { // grant access to premium features } } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ```csharp showLineNumbers Adapty.GetProfile((profile, error) => { if (error != null) { // handle the error return; } // check the access if (profile.AccessLevels["YOUR_ACCESS_LEVEL"]?.IsActive ?? false) { // grant access to premium features } }); ``` ```kotlin showLineNumbers Adapty.getProfile() .onSuccess { profile -> // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive == true) { // grant access to premium features } } .onError { error -> // handle the error } ``` ```typescript showLineNumbers try { const profile = await adapty.getProfile(); // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive) { // grant access to premium features } } catch (error) { // handle the error } ``` --- # File: grant-access-level --- --- title: "Grant access levels manually" description: "Unlock paid features manually for specific users or user groups" --- If you need to **manually unlock premium features** for specific users or user groups, you can do it using the Adapty API. This is useful for promotional campaigns, investor access, or special customer support cases. In this guide, you'll learn how to identify users and grant them access levels programmatically. #### Sample use cases - **Promo codes**: When users enter a valid promo code in your app, automatically grant them access to premium features. - **Investor/beta tester access**: Provide premium access to investors or beta testers by checking their custom attributes. ## Step 1. Identify users Adapty uses `customer_user_id` to identify users across platforms and devices. This is crucial for ensuring users keep their access after reinstalling the app or switching devices. You need to create this ID once. When users first sign up from the app, you can pass their customer user ID during SDK activation, or use the `identify` method if the SDK was activated before signup. :::important If you identify new users after SDK activation, the SDK will first create an anonymous profile (it can't work without one). When you call `identify` with a customer user ID, a new profile will be created. This behavior is normal and won't affect analytics accuracy. Read more [here](ios-quickstart-identify.md). ::: ```swift showLineNumbers do { try await Adapty.identify("YOUR_USER_ID") // Unique for each user } catch { // handle the error } ``` ```swift showLineNumbers // User IDs must be unique for each user Adapty.identify("YOUR_USER_ID") { error in if let error { // handle the error } } ``` ```kotlin showLineNumbers Adapty.identify("YOUR_USER_ID") { error -> // Unique for each user if (error == null) { // successful identify } } ``` ```java showLineNumbers // User IDs must be unique for each user Adapty.identify("YOUR_USER_ID", error -> { if (error == null) { // successful identify } }); ``` ```typescript showLineNumbers try { await adapty.identify("YOUR_USER_ID"); // Unique for each user // successfully identified } catch (error) { // handle the error } ``` ```dart showLineNumbers try { await Adapty().identify(customerUserId); // Unique for each user } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ```csharp showLineNumbers Adapty.Identify("YOUR_USER_ID", (error) => { // Unique for each user if(error == null) { // successful identify } }); ``` ```kotlin showLineNumbers Adapty.identify("YOUR_USER_ID") // Unique for each user .onSuccess { // successful identify } .onError { error -> // handle the error } ``` ```typescript showLineNumbers try { await adapty.identify({ customerUserId: "YOUR_USER_ID" }); // successfully identified } catch (error) { // handle the error } ``` ## Step 2. Grant access level via API Once a user is identified with a `customer_user_id`, you can grant them access levels using the server-side API. This API call will grant the access level to the user, so they can access paid features without actually paying. See the full method reference [here](api-adapty/operations/grantAccessLevel). :::tip You can control user access by adding a custom attribute (e.g., Beta tester or Investor) in the Adapty dashboard. When your app launches, [check this attribute in the user’s profile](subscription-status.md) to grant access automatically. To update access, just change the attribute in the dashboard. ::: ```curl curl --request POST \ --url https://api.adapty.io/api/v2/server-side-api/purchase/profile/grant/access-level/ \ --header 'Accept: application/json' \ --header 'Authorization: Api-Key YOUR_SECRET_API_KEY' \ --header 'Content-Type: application/json' \ --header 'adapty-customer-user-id: CUSTOMER_USER_ID' \ --data '{ "access_level_id": "YOUR_ACCESS_LEVEL" }' ``` ## Step 3. Verify access in the app After granting access via API, the user's profile will be automatically updated. Fetch their profile to check their subscription status and unlock premium features. ```swift showLineNumbers do { let profile = try await Adapty.getProfile() if profile.accessLevels["YOUR_ACCESS_LEVEL_ID"]?.isActive ?? false { // grant access to premium features } } catch { // handle the error } ``` ```swift showLineNumbers Adapty.getProfile { result in if let profile = try? result.get() { // check the access if profile.accessLevels["YOUR_ACCESS_LEVEL_ID"]?.isActive ?? false { // grant access to premium features } } } ``` ```kotlin showLineNumbers Adapty.getProfile { result -> when (result) { is AdaptyResult.Success -> { val profile = result.value // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL_ID"]?.isActive == true) { // grant access to premium features } } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers Adapty.getProfile(result -> { if (result instanceof AdaptyResult.Success) { AdaptyProfile profile = ((AdaptyResult.Success) result).getValue(); // check the access if (profile.getAccessLevels().get("YOUR_ACCESS_LEVEL_ID") != null && profile.getAccessLevels().get("YOUR_ACCESS_LEVEL_ID").getIsActive()) { // grant access to premium features } } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` ```typescript showLineNumbers try { const profile = await adapty.getProfile(); // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL_ID"]?.isActive) { // grant access to premium features } } catch (error) { // handle the error } ``` ```dart showLineNumbers try { final profile = await Adapty().getProfile(); // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL_ID"]?.isActive ?? false) { // grant access to premium features } } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ```csharp showLineNumbers Adapty.GetProfile((profile, error) => { if (error != null) { // handle the error return; } // check the access if (profile.AccessLevels["YOUR_ACCESS_LEVEL_ID"]?.IsActive ?? false) { // grant access to premium features } }); ``` ```kotlin showLineNumbers Adapty.getProfile() .onSuccess { profile -> // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL_ID"]?.isActive == true) { // grant access to premium features } } .onError { error -> // handle the error } ``` ```typescript showLineNumbers try { const profile = await adapty.getProfile(); // check the access if (profile.accessLevels["YOUR_ACCESS_LEVEL_ID"]?.isActive) { // grant access to premium features } } catch (error) { // handle the error } ``` --- # File: migration-guide-to-server-side-API-v2 --- --- title: "Migration guide to server-side API v2" description: "" --- Adapty's Server-Side API v2 introduces new capabilities and improvements to help you manage access levels, transactions, and user profiles more effectively. ## Why Migrate? The second version of the Server-Side API gives you more flexibility and features when working with Adapty: - **Separate access level management**: Assign access levels to users without requiring transaction details, making it easier to handle compensations, bonuses, or other non-transactional scenarios. - **Record one-time purchases**: Log transactions for consumable product purchases by providing consumable-product-specific fields. - **Enhanced transaction details:** Include more data with transactions, like refunds, billing issues, cancellation reasons, renewals, and more. - **Profile updates:** Instead of just adding attributes, you can update a user’s profile. For instance, you can add installation metadata or disable external analytics if needed. Although v1 is still supported, we recommend moving to v2 for expanded functionality and ongoing updates. ## Changes Overview | Change | Required action | | ----------------------------------- | ------------------------------------------------------------ | | **Base URL and Endpoint** | Base URL and all endpoints are changed. Update your configuration as described in the request endpoints | | **Request Headers** |
  1. Add either `adapty-profile-id` or `adapty-customer-user-id` as a header.
  2. Add a new `adapty-platform` header.
| | **Request and Response Structure** | Modify parameters as outlined for each request and update your integration to handle the [new response formats](api-responses). | | **Changed access level management** | The old [Prolong/grant a subscription for a user](server-side-api-specs-legacy#prolonggrant-a-subscription-for-a-user) request is now split into three:
  • [Set Transaction](api-adapty/operations/setTransaction): Add transaction details with adding access.
  • [Grant Access Level](api-adapty/operations/grantAccessLevel): Add or extend access without transaction.
  • [Revoke Access Level](api-adapty/operations/revokeAccessLevel): Shorten or revoke access without transaction.
| ## Migration Steps :::note To simplify using our server-side API, we've prepared a Postman collection and an environment file you can download and import into Postman. [Download the collection and environment](https://raw.githubusercontent.com/adaptyteam/adapty-docs/refs/heads/main/Downloads/Adapty_server_side_API_postman_collection.zip) ::: ### Step 1. Configure request headers Add new request headers as follows: | **Header** | **Description** | | --------------------------- | ------------------------------------------------------------ | | **adapty-profile-id** | (Required, choose one) The user’s Adapty profile ID. Visible in the **Adapty ID** field in the [Adapty Dashboard -> **Profiles**](https://app.adapty.io/profiles/users) -> specific profile page. Interchangeable with **adapty-customer-user-id**, use any of them. | | **adapty-customer-user-id** |

(Required, choose one) The user’s ID in your system. Visible in the **Customer user ID** field in the [Adapty Dashboard -> **Profiles**](https://app.adapty.io/profiles/users) -> specific profile page. Interchangeable with **adapty-profile-id**, use any of them.

⚠️ Works only if you [identify users](identifying-users) in your app code using the Adapty SDK.

| | **adapty-platform** | Specify the app's platform. Possible options: `iOS`, `macOS`, `iPadOS`, `visionOS`, `Android`. | --- ### Step 2. Change transaction and access management requests In version 1, you used to use: - [Prolong/Grant a Subscription for a User](server-side-api-specs-legacy#prolonggrant-a-subscription-for-a-user) request: to record a transaction and grant or shorten access level. - [Revoke access level](server-side-api-specs-legacy#revoke-subscription-from-a-user) request: to immediately revoke access. They are now replaced with three separate requests to distinguish between adding transactions and managing access levels: 1. **[Grant Access Level](api-adapty/operations/grantAccessLevel):** Use this request to extend an access level without linking it to a transaction. 2. **[Revoke Access Level](api-adapty/operations/revokeAccessLevel):** to immediately revoke or shorten access. 3. **[Set Transaction](api-adapty/operations/setTransaction):** Use this request to add transaction details to Adapty with access levels. --- #### Step 2.1. How to grant access level :::info For a detailed description, refer to the [Grant access level](api-adapty/operations/grantAccessLevel) request. ::: In version 1, the [Prolong/grant a subscription for a user](server-side-api-specs-legacy#prolonggrant-a-subscription-for-a-user) request was used to grant access. Now you can grant access with the [Grant access level](api-adapty/operations/grantAccessLevel) request without providing transaction details. - **Endpoint:** `https://api.adapty.io/api/v2/server-side-api/grant/access-level/` - Parameters to keep: - **access_level_id**: Previously in the endpoint. Required. - **starts_at**: Now nullable. - **expires_at**: Optional for lifetime access and nullable. --- #### Step 2.2. How to revoke or shorten access level :::info For a detailed description, refer to the [Revoke access level](api-adapty/operations/revokeAccessLevel) request. ::: In version 1, you used the [Revoke access level](server-side-api-specs-legacy#revoke-subscription-from-a-user) request to immediately revoke access and the [Prolong/Grant a Subscription for a User](server-side-api-specs-legacy#prolonggrant-a-subscription-for-a-user) request to shorten it. Now you can use the [Revoke access level](api-adapty/operations/revokeAccessLevel) request for both actions. - **Endpoint:** `https://api.adapty.io/api/v2/server-side-api/purchase/profile/revoke/access-level/` - Parameters to keep: - **access_level_id**: Required. - **expires_at**: Nullable unless access is revoked immediately. --- #### Step 2.3. How to record a subscription transaction :::info For a detailed description, refer to the [Set transaction](api-adapty/operations/setTransaction) request. ::: In version 1, transactions were recorded using the [Prolong/Grant a Subscription for a User](server-side-api-specs-legacy#prolonggrant-a-subscription-for-a-user) request, which was limited to subscription transactions. In version 2, this functionality has been replaced by the [Set Transaction](api-adapty/operations/setTransaction) request. This request can handle both subscription transactions and one-time purchases. - **Endpoint:** `https://api.adapty.io/api/v2/server-side-api/purchase/set/transaction/` - **Details:** The parameters required vary based on whether the transaction is a subscription or a one-time purchase. See the guidelines below for recording subscription transactions. **New fields** | **Parameter** | **Change** | **Type** | **Required** | **Nullable** | **Description** | | --------------------------- | ---------- | ------------- | ------------------ | ------------------ | ------------------------------------------------------------ | | `billing_issue_detected_at` | Added | ISO 8601 date | :heavy_plus_sign: | :heavy_plus_sign: | The datetime when a billing issue was detected (e.g., a failed card charge). Subscription might still be active. This is cleared if the payment goes through. | | `cancellation_reason` | Added | String | :heavy_plus_sign: | :heavy_plus_sign: | Possible reasons for cancellation include: `voluntarily_cancelled`, `billing_error`, `price_increase`, `product_was_not_available`, `refund`, `upgraded`, or `unknown`. | | `environment` | Added | String | :heavy_minus_sign: | :heavy_minus_sign: |

Environment where the transaction took place. Options are `Sandbox` or `Production.`

Replaces the `is_sandbox` parameter.

| | `grace_period_expires_at` | Added | ISO 8601 date | :heavy_minus_sign: | :heavy_minus_sign: | The datetime when the [grace period](https://developer.apple.com/news/?id=09122019c) will end, if the subscription is currently in one. | | `is_family_shared` | Added | Boolean | :heavy_minus_sign: | :heavy_minus_sign: | A Boolean value indicating whether the product supports family sharing in App Store Connect. iOS only. Always `false` for iOS below 14.0 and macOS below 11.0. | | `offer` | Added | Object | :heavy_plus_sign: | :heavy_minus_sign: | Represents the purchase offer as an object. See the [Offer](server-side-api-objects#offer) object. | | `originally_purchased_at` | Added | ISO 8601 date | :heavy_plus_sign: | :heavy_minus_sign: | For subscription chains, this is the purchase date of the original transaction, linked by `store_original_transaction_id`. | | `purchase_type` | Added | String | :heavy_plus_sign: | :heavy_minus_sign: | Specifies product type, here set to `subscription`. | | `purchased_at` | Added | ISO 8601 date | :heavy_plus_sign: | :heavy_minus_sign: | Indicates most recent purchase date. | | `refunded_at` | Added | ISO 8601 date | :heavy_minus_sign: | :heavy_minus_sign: | Indicates subscription refund datetime if applicable. | | `renew_status` | Added | Boolean | :heavy_plus_sign: | :heavy_minus_sign: | Indicates if subscription auto-renewal is enabled. | | `renew_status_changed_at` | Added | ISO 8601 date | :heavy_minus_sign: | :heavy_minus_sign: | Indicates when auto-renewal status changed. | | `variation_id` | Added | String | :heavy_minus_sign: | :heavy_minus_sign: | The variation ID used to trace purchases to the specific paywall they were made from. | **Removed fields** | **Parameter** | **Change** | **Description** | | ------------------------- | ---------- | ------------------------------------------------------------ | | `base_plan_id` | Removed | Removed. Add the base plan ID to the `store_product_id` field in the format `product_id:base_plan_id`. | | `duration_days` | Removed | Removed as not needed. The duration is calculated automatically. | | `introductory_offer_type` | Removed | Offer types are now in the `offer` object. | | `is_lifetime` | Removed | Removed as it's replaced with the `purchase_type` parameter. | | `is_sandbox` | Removed | Replaced with the `environment` parameter. | | `price_locale` | Removed | Moved to the `price` object. | | `proceeds` | Removed | | | `starts_at` | Removed | Removed as it will be automatically taken from the access level connected to the selected product. | **Changed fields** | **Parameter** | **Change** | **Type** | **Required** | **Nullable** | Change Description | | ------------------------------------------------------------ | ---------------- | --------------- | --------------------------------------- | ------------------ | ------------------------------------------------------------ | | `price` | Changed | Float -> Object | :heavy_minus_sign: -> :heavy_plus_sign: | :heavy_minus_sign: | Now represented as a [Price](server-side-api-objects#price) object and includes `price_locale`, `country`, and `value` fields. | | `store` | Changed | String | :heavy_minus_sign: -> :heavy_plus_sign: | :heavy_minus_sign: |
  1. The field became mandatory.
  2. In addition to standard app stores, you can now use custom stores as well.
| | `vendor_original_transaction_id` -> `store_original_transaction_id` | Renamed, changed | String | :heavy_minus_sign: -> :heavy_plus_sign: | :heavy_minus_sign: |
  1. The field is renamed.
  2. The field became mandatory.
| | `vendor_product_id` -> `store_product_id` | Renamed, changed | String | :heavy_minus_sign: -> :heavy_plus_sign: | :heavy_minus_sign: |
  1. The field is renamed.
  2. The field became mandatory.
| | `vendor_transaction_id` -> `store_transaction_id` | Renamed, changed | String | :heavy_minus_sign: -> :heavy_plus_sign: | :heavy_minus_sign: |
  1. The field is renamed.
  2. The field became mandatory.
| --- #### Step 2.4. How to record a one-time purchase transaction :::info For a detailed description, refer to the [Set transaction](api-adapty/operations/setTransaction) request. ::: In version 1, transactions were recorded using the [Prolong/Grant a Subscription for a User](server-side-api-specs-legacy#prolonggrant-a-subscription-for-a-user) request, which was limited to subscription transactions. In version 2, this functionality has been replaced by the [Set Transaction](api-adapty/operations/setTransaction) request. This request can handle both subscription transactions and one-time purchases. - **Endpoint:** `https://api.adapty.io/api/v2/server-side-api/purchase/set/transaction/` - **Details:** The parameters required vary based on whether the transaction is a subscription or a one-time purchase. See the guidelines below for recording one-time purchase transactions. **New fields** | **Parameter** | **Change** | **Type** | **Required** | **Nullable** | **Description Change** | | -------------------------------- | ---------- | --------------- | --------------------------------------- | ------------------ | ------------------------------------------------------------ | | `cancellation_reason` | Added | String | :heavy_minus_sign: | :heavy_minus_sign: | Possible reasons for cancellation: voluntarily_cancelled, billing_error, price_increase, product_was_not_available, refund, cancelled_by_developer, new_subscription_replace, upgraded, unknown, adapty_revoked. | | `environment` | Added | String | :heavy_minus_sign: | :heavy_minus_sign: |

Environment where the transaction took place. Options are `Sandbox` or `Production.`

Replaces the `is_sandbox` parameter.

| | `is_family_shared` | Added | Boolean | :heavy_minus_sign: | :heavy_minus_sign: | Indicates whether the product supports family sharing in App Store Connect. iOS only. Always false for iOS below 14.0 and macOS below 11.0. | | `offer` | Added | Object | :heavy_minus_sign: | :heavy_minus_sign: | Represents the purchase offer as an object. See the [Offer](server-side-api-objects#offer) object. | | `purchase_type` | Added | String | :heavy_plus_sign: | :heavy_minus_sign: | Specifies the type of product purchased. Here set to `one_time_purchase`. | | `purchased_at` | Added | ISO 8601 date | :heavy_plus_sign: | :heavy_minus_sign: | The datetime when the access level was last purchased. | | `refunded_at` | Added | ISO 8601 date | :heavy_minus_sign: | :heavy_minus_sign: | If refunded, shows the datetime of the refund. | | `variation_id` | Added | String | :heavy_minus_sign: | :heavy_minus_sign: | The variation ID used to trace purchases to the specific paywall they were made from. | **Removed fields** | **Parameter** | **Change** | **Description Change** | | ------------------------- | ---------- | ------------------------------------------------------------ | | `base_plan_id` | Removed | Removed. Add the base plan ID to the `store_product_id` field in the format `product_id:base_plan_id`. | | `duration_days` | Removed | Removed as not needed. The duration is calculated automatically. | | `expires_at` | Removed | Removed as not relevant to a one-time purchase. | | `introductory_offer_type` | Removed | Offer types are now in the `offer` object. | | `is_lifetime` | Removed | Removed as it's replaced with the `purchase_type` parameter. | | `is_sandbox` | Removed | Replaced with the `environment`parameter. | | `price_locale` | Removed | Moved to the `price` object. | | `proceeds` | Removed | | | `starts_at` | Removed | Removed as not relevant to a one-time purchase. | **Changed fields** | **Parameter** | **Change** | **Type** | **Required** | **Nullable** | **Description Change** | | ------------------------------------------------------------ | ---------------- | --------------- | --------------------------------------- | ------------------ | ------------------------------------------------------------ | | `price` | Changed | Float -> Object | :heavy_minus_sign: -> :heavy_plus_sign: | :heavy_minus_sign: | Now represented as a [Price](server-side-api-objects#price) object and includes `price_locale`, `country`, and `value` fields. | | `store` | Changed | String | :heavy_minus_sign: -> :heavy_plus_sign: | :heavy_minus_sign: |
  1. The field became mandatory.
  2. In addition to standard app stores, you can now use custom stores as well.
| | `vendor_original_transaction_id` -> `store_original_transaction_id` | Renamed, changed | String | :heavy_minus_sign: -> :heavy_plus_sign: | :heavy_minus_sign: |
  1. The field is renamed.
  2. The field became mandatory.
| | `vendor_product_id` -> `store_product_id` | Renamed, changed | String | :heavy_minus_sign: -> :heavy_plus_sign: | :heavy_minus_sign: |
  1. The field is renamed.
  2. The field became mandatory.
| | `vendor_transaction_id` -> `store_transaction_id` | Renamed, changed | String | :heavy_minus_sign: -> :heavy_plus_sign: | :heavy_minus_sign: |
  1. The field is renamed.
  2. The field became mandatory.
| --- ### Step 3. Change `Get info about a user` request :::info For a detailed description, refer to the [Get profile](api-adapty/operations/getProfile) request. ::: Change the request as follows: - **Endpoint:** `https://api.adapty.io/api/v2/server-side-api/profile/` - **Change:** The User Profile ID or Customer User ID is now passed via headers, and no additional parameters are needed. The **extended** parameter is no longer needed as the complete profile data is always returned. --- ### Step 4. Change `Set the user's attribute` request :::info For a detailed description, refer to the [Update profile](api-adapty/operations/updateProfile) request. ::: In version 1, you could only update user attributes. With version 2, you can modify a wider range of profile fields, such as installation metadata or analytics settings. - **Endpoint:** `https://api.adapty.io/api/v2/server-side-api/profile/`. | **Parameter** | **Change** | **Type** | **Required** | **Nullable** | **Description Change** | | -------------------- | ---------- | --------------------- | ------------------ | --------------------------------------- | ------------------------------------------------------------ | | `analytics_disabled` | Added | Boolean | :heavy_minus_sign: | :heavy_minus_sign: | Option to opt out of external analytics. When disabled, events won’t be sent to integrations, and `idfa`, `idfv`, and `advertising_id` become nullable. | | `installation_meta` | Added | Dictionary | :heavy_minus_sign: | :heavy_minus_sign: | Contains app-specific device details as a dictionary of [Installation Meta](server-side-api-objects#installation-meta) objects. | | `store` | Added | String | :heavy_minus_sign: | :heavy_plus_sign: | Store where the product was bought. Options include **app_store**, **play_store**, **stripe**, or the name of your [custom store](custom-store). | | `store_country` | Added | String | :heavy_minus_sign: | :heavy_plus_sign: | Country of the end user app store. | | `birthday` | Changed | Date -> ISO 8601 date | :heavy_minus_sign: | :heavy_plus_sign: -> :heavy_minus_sign: | Your end user's birthday. | | `ip_country` | Changed | String | :heavy_minus_sign: | :heavy_plus_sign: -> :heavy_minus_sign: | Country of the end user in ISO 3166-2 format. Must be passed if the request is made from the server. Otherwise, determined by request IP. | --- ### Step 5. Change `Delete user's data` request :::info For a detailed description, refer to the [Delete profile](api-adapty/operations/deleteProfile) request. ::: - **Endpoint:** `https://api.adapty.io/api/v2/server-side-api/profile/` - **Change:** The `adapty-profile-id` or `adapty-customer-user-id` is now passed via headers, and no additional parameters are needed. --- # File: web-api --- --- title: Adapty Web API description: "" --- The Web API is an extension of the server-side API designed for use with web apps. It allows you to retrieve the correct paywall using its related placement ID and record paywall views for accurate conversion tracking. This helps you use the A/B testing and paywall personalization available within Adapty, as well as track which paywalls work the best. ## Use case: Record a transaction from your web app and link it to the used paywall Let's say you sell products in your web app. You need to display a paywall to your users, let them purchase a product, and then add the transaction details to Adapty. It’s essential to link these transactions to the specific paywalls through which the user made the purchase so that your analytics reflects accurate data. This can be easily accomplished using the Adapty API ### Prerequisites 1. [Create the products](create-product) you’ll use in the paywall within the Adapty Dashboard. 2. [Create the paywall](create-paywall) in the Adapty Dashboard. [Use remote config](customize-paywall-with-remote-config) to design your web paywall. 3. [Set up a placement](create-placement) and link the paywall to it in the Adapty Dashboard. ### Steps with Adapty API 1. **Create a user profile:** Adapty relies on having a profile before requesting a paywall to personalize the end result to the user who requested it. Use the [Create profile](api-adapty/operations/createProfile) request to create a user profile. 2. **Fetch and display the paywall:** When the user reaches the placement in your web app where the paywall should be shown, use the [Get paywall](api-web/operations/getPaywall) request to retrieve the paywall via the [placement ID](placements). As a result, you'll get a paywall for the [audience](audience) corresponding to your user. Display the paywall with your code, using the returned products and (optionally) this paywall's [remote config](customize-paywall-with-remote-config). 3. **Record the paywall view:** Use the [Record paywall view](api-web/operations/recordPaywallView) to log the paywall view with Adapty to ensure your analytics accurately reflect the event. This is vital to track conversions correctly. 4. **Record the purchase:** If the user completes a purchase, send the transaction details to Adapty using the Adapty API. Include the **variation ID** in this request to link the transaction to the specific paywall displayed. For guidance, check out our page on [associating paywalls with transactions in mobile apps](report-transactions-observer-mode)—the same approach applies to web apps. 5. **Add marketing attribution data (if applicable):** If you have any marketing attribution data (e.g., campaign or ad details), use the [Add attribution](api-web/operations/addAttribution) to merge it into the user profile to enrich the analytics and learn more about your ad performance in Adapty. --- **What's next:** - Proceed with [Web API authorization](web-api-authorization) - Requests: - [Add attribution](api-web/operations/addAttribution) - [Get paywall](api-web/operations/getPaywall) - [Record paywall view](api-web/operations/recordPaywallView) --- # File: web-api-authorization --- --- title: Authorization and Request format for Web API description: "" --- ## Authorization API requests must be authenticated by your public API key as the **Authorization** header with the value `Api-Key {your_public_api_key}`, for example, `Api-Key public_live_...`. Find this key in the [Adapty Dashboard -> **App Settings** -> **General** tab -> **API keys** section](https://app.adapty.io/settings/general). ## Request format - **Content-Type header**: Set the **Content-Type** header to `application/json` for the API to process your request. - **Body**: The API expects the request to use the body as JSON. --- # File: web-api-requests --- --- title: " Web API Requests" description: "" --- Adapty's server-side API empowers you to programmatically access and manage your subscription data, enabling seamless integration with your existing services and infrastructure. Whether you're syncing data across platforms, granting access levels, or validating purchases in Stripe, this API provides the tools to keep your systems in sync and your users engaged. ## Postman collection and environment To simplify using our web API, we've prepared a Postman collection and an environment file you can download and import into Postman. - **Request Collection**: Includes all requests available in the Adapty web API. Note that it uses variables that you can define in the environment. - **Environment**: Contains a list of variables where you can define values once. We've prepared a unified environment for the server-side API, web API, and analytics export API to make things easier for you. After making this environment active, Postman will automatically substitute the defined variable values in your requests. :::tip [Download the collection and environment](https://raw.githubusercontent.com/adaptyteam/adapty-docs/refs/heads/main/Downloads/Adapty_Web_API_postman_collection.zip) ::: For info on how to import a collection and environment to Postman, please refer to the [Postman documentation](https://learning.postman.com/docs/getting-started/importing-and-exporting/importing-data/). ## Variables used We've created a unified environment for the server-side API, web API, and analytics export API to simplify your workflow. Below are the variables specific to the web API: | Variable | Description | Example Value | | ----------------------- | ------------------------------------------------------------ | ------------------------------------------------------- | | public_api_key | You can find it in the **Public SDK key** field in the [**App settings**](https://app.adapty.io/settings/general). | `public_live_Pj1P1xzM.2CvSvE1IalQRFjsWy6csBVNpH33atnod` | | adapty-customer-user-id | The user ID used in your system. In the Adapty Dashboard, you can find it in the **Customer user ID** field of the Profile. | `john.doe@example.com` | | adapty-profile-id | The user ID assigned in Adapty. In the Adapty Dashboard, you can find it in the **Adapty ID** field of the Profile. | `3286abd3-48b0-4e9c-a5f6-ac0a006333a6` | **What's next: Requests:** - [Get paywall](api-web/operations/getPaywall) - [Record paywall view](api-web/operations/recordPaywallView) - [Add attribution](api-web/operations/addAttribution) --- # File: export-analytics-api --- --- title: Exporting analytics with API --- Exporting your analytics data to CSV gives you the flexibility to dive deeper into your app’s performance metrics, customize reports, and analyze trends over time. With the Adapty API, you can easily pull detailed analytics into a CSV format, making it convenient to track, share, and refine your data insights as needed. ## Getting started with the API for analytics export With the analytics export API, you can, for example: 1. **Analyze MRR from Marketing Campaigns**: Measure the impact of last year's marketing campaigns in a specific country to see which ones brought in the highest revenue, with weekly tracking. Use the [Retrieve analytics data](api-export-analytics/operations/retrieveAnalyticsData) method for this. 2. **Track Cohort Retention Over Time**: Follow retention by cohort to spot drop-off points and compare cohorts over time, revealing trends and key moments where engagement strategies could boost retention. Limited to a specific app store, a specific country, and a particular product. Use the [Retrieve cohort data](api-export-analytics/operations/retrieveCohortData) method for this. 3. **Evaluate Conversion Rates Across Channels**: Analyze conversion rates for key acquisition channels to see which are most effective in driving first-time purchases. This helps prioritize marketing spending on high-performing channels. Use the [Retrieve conversion data](api-export-analytics/operations/retrieveConversionData) method for this. 4. **Review Churn Rate**: Monitor how quickly users are unsubscribing to uncover churn patterns or gauge the success of retention efforts, focusing on a specific country and a specific product. Use the [Retrieve funnel data](api-export-analytics/operations/retrieveFunnelData) method for this. 5. **Assess LTV by User Segment**: Identify the lifetime value of different user segments to understand which groups bring in the highest revenue over time. Focus on high-value segments like long-term subscribers, and use the results to refine acquisition strategies. Use the [Retrieve LTV data](api-export-analytics/operations/retrieveLTVData) method for this. 6. **Check Retention by Country**: Look at retention rates by region to find high-engagement markets and guide localization or regional strategies. Use the [Retrieve retention data](api-export-analytics/operations/retrieveRetentionData) method for this. --- **What's next**: - [Authorization and request format](export-analytics-api-authorization) - [Exporting analytics API requests](export-analytics-api-requests) --- # File: export-analytics-api-authorization --- --- title: Authorization and request format for Exporting analytics API --- ## Authorization You need to authenticate your API requests with your secret API key as an Authorization header. You can find it in the [App Settings](https://app.adapty.io/settings/general). The format is `Api-Key {YOUR_SECRET_API_KEY}`, for example: `Api-Key secret_live_...`. ## Request format **Headers** The server-side API requests require specific headers and a JSON body. Use the details below to structure your requests: | Header | Description | | ------------ | ------------------------------------------------------------ | | Content-Type | (Required) Set to `application/json` for the API to process the request. | | Adapty-Tz | (Optional) Set the timezone to define how the data is grouped and displayed. Use the [IANA Time Zone Database format](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) (e.g., `Europe/Berlin`). | ## Body The API expects a JSON-formatted body with the necessary data for the request. --- **What's next: Requests:** - [Retrieve analytics data](api-export-analytics/operations/retrieveAnalyticsData) - [Retrieve cohort data](api-export-analytics/operations/retrieveCohortData) - [Retrieve conversion data](api-export-analytics/operations/retrieveConversionData) - [Retrieve funnel data](api-export-analytics/operations/retrieveFunnelData) - [Retrieve Lifetime Value (LTV) data](api-export-analytics/operations/retrieveLTVData) - [Retrieve retention data](api-export-analytics/operations/retrieveRetentionData) --- # File: export-analytics-api-requests --- --- title: Exporting analytics API requests --- Exporting your analytics data to CSV gives you the flexibility to dive deeper into your app’s performance metrics, customize reports, and analyze trends over time. With the Adapty API, you can easily pull detailed analytics into a CSV format, making it convenient to track, share, and refine your data insights as needed. ## Postman collection and environment To simplify using our API for exporting analytics data, we've prepared a Postman collection and an environment file you can download and import into Postman. - **Request Collection**: Includes all requests available in the Adapty analytics export API. Note that it uses variables that you can define in the environment. - **Environment**: Contains a list of variables where you can define values once. We've prepared a unified environment for the server-side API, web API, and analytics export API to make things easier for you. After making this environment active, Postman will automatically substitute the defined variable values in your requests. :::tip [Download the collection and environment](https://raw.githubusercontent.com/adaptyteam/adapty-docs/refs/heads/main/Downloads/Adapty_export_analytics_API_postman_collection.zip) ::: For info on how to import a collection and environment to Postman, please refer to the [Postman documentation](https://learning.postman.com/docs/getting-started/importing-and-exporting/importing-data/). ### Variables used We've created a unified environment for the server-side API, web API, and analytics export API to simplify your workflow. Below are the variables specific to the analytics export API: | Variable | Description | Example Value | | ----------------------- | ------------------------------------------------------------ | ------------------------------------------------------- | | secret_api_key | You can find it in the **Secret key** field in the [**App settings**](https://app.adapty.io/settings/general). | `secret_live_Pj1P1xzM.2CvSvE1IalQRFjsWy6csBVNpH33atnod` | **Requests:** - [Retrieve analytics data](api-export-analytics/operations/retrieveAnalyticsData) - [Retrieve cohort data](api-export-analytics/operations/retrieveCohortData) - [Retrieve conversion data](api-export-analytics/operations/retrieveConversionData) - [Retrieve funnel data](api-export-analytics/operations/retrieveFunnelData) - [Retrieve Lifetime Value (LTV) data](api-export-analytics/operations/retrieveLTVData) - [Retrieve retention data](api-export-analytics/operations/retrieveRetentionData) --- # End of Documentation _Generated on: 2026-03-05T16:27:47.999Z_ _Successfully processed: 14/14 files_ # CAPACITOR - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Generated on: 2026-03-05T16:27:48.000Z Total files: 37 --- # File: capacitor-sdk-overview --- --- title: "Capacitor SDK overview" description: "Learn about Adapty Capacitor SDK and its key features." --- [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-Capacitor.svg?style=flat&logo=capacitor)](https://github.com/adaptyteam/AdaptySDK-Capacitor/releases) Welcome! We're here to make in-app purchases a breeze 🚀 We've built the [Adapty Capacitor SDK](https://github.com/adaptyteam/AdaptySDK-Capacitor/) to take the headache out of in-app purchases so you can focus on what you do best – building amazing apps. Here's what we handle for you: - Handle purchases, receipt validation, and subscription management out of the box - Create and test paywalls without app updates - Get detailed purchase analytics with zero setup - cohorts, LTV, churn, and funnel analysis included - Keep the user subscription status always up to date across app sessions and devices - Integrate your app with marketing attribution and analytics services using just one line of code :::note Before diving into the code, you'll need to integrate Adapty with App Store Connect and Google Play Console, then set up products in the dashboard. Check out our [quickstart guide](quickstart.md) to get everything configured first. ::: ## Get started :::tip Our docs are optimized for use with LLMs. Check out [this article](adapty-cursor-capacitor.md) to learn how to get the best results when integrating the Adapty SDK using AI with our docs. ::: Here's what we'll cover in the integration guide: 1. [Install & configure SDK](sdk-installation-capacitor.md): Add the SDK as a [dependency](https://www.npmjs.com/package/@adapty/capacitor) to your project and activate it in the code. 2. [Enable purchases through paywalls](capacitor-quickstart-paywalls.md): Set up the purchase flow so users can buy products. 3. [Check the subscription status](capacitor-check-subscription-status.md): Automatically check the user's subscription state and control their access to paid content. 4. [Identify users (optional)](capacitor-quickstart-identify.md): Associate users with their Adapty profiles to ensure their data is stored consistently across devices. ### See it in action Want to see how it all comes together? We've got you covered: **Sample apps**: Check out our complete examples that demonstrate the full setup: - [React](https://github.com/adaptyteam/AdaptySDK-Capacitor/tree/master/examples/basic-react-example) - [Vue.js](https://github.com/adaptyteam/AdaptySDK-Capacitor/tree/master/examples/basic-vue-example) - [Angular](https://github.com/adaptyteam/AdaptySDK-Capacitor/tree/master/examples/basic-angular-example) - [Advanced development tools](https://github.com/adaptyteam/AdaptySDK-Capacitor/tree/master/examples/adapty-devtools) ## Main concepts Before diving into the code, let's get familiar with the key concepts that make Adapty work. The beauty of Adapty's approach is that only placements are hardcoded in your app. Everything else – products, paywall designs, pricing, and offers – can be managed flexibly from the Adapty dashboard without app updates: 1. **Product** - Anything available for purchase in your app – subscription, consumable product, or lifetime access. 2. **Paywall** - The only way to retrieve products from Adapty and use it to its full power. We've designed it this way to make it easier to track how different product combinations affect your monetization metrics. A paywall in Adapty serves as both a specific set of your products and the visual configuration that accompanies them. 3. **Placement** - A strategic point in your user journey where you want to show a paywall. Think of placements as the "where" and "when" of your monetization strategy. Common placements include: - `main` - Your primary paywall location - `onboarding` - Shown during the user onboarding flow - `settings` - Accessible from your app's settings Start with the basics like `main` or `onboarding` for your first integration, then think about where else in your app users might be ready to purchase. 4. **Profile** - When users purchase a product, their profile is assigned an **access level** which you use to define access to paid features. --- # File: sdk-installation-capacitor --- --- title: "Capacitor - Adapty SDK installation & configuration" description: "Step-by-step guide on installing Adapty SDK on Capacitor for subscription-based apps." --- Adapty SDK includes two key modules for seamless integration into your Capacitor app: - **Core Adapty**: This module is required for Adapty to function properly in your app. - **AdaptyUI**: This module is needed if you use the [Adapty Paywall Builder](adapty-paywall-builder), a user-friendly, no-code tool for easily creating cross-platform paywalls. AdaptyUI is automatically activated along with the core module. :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](https://github.com/adaptyteam/AdaptySDK-Capacitor/tree/master/examples), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ## Requirements The [Adapty Capacitor SDK](https://github.com/adaptyteam/AdaptySDK-Capacitor/) has the following version requirements: | Adapty SDK Version | Capacitor Version | iOS Version | |--------------------|-------------------|-------------| | 3.16.0+ | 8 | 15.0+ | | 3.15 | 7 | 14.0+ | Capacitor versions 6 and below are not supported. :::info Adapty is compatible with Google Play Billing Library up to 8.x. By default, Adapty works with Google Play Billing Library v.7.0.0 but, if you want to force a later version, you can manually [add the dependency](https://developer.android.com/google/play/billing/integrate#dependency). ::: ## Install Adapty SDK [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-Capacitor.svg?style=flat&logo=capacitor)](https://github.com/adaptyteam/AdaptySDK-Capacitor/releases) Install Adapty SDK: ```sh npm install @adapty/capacitor npx cap sync ``` ## Activate Adapty module of Adapty SDK :::note The Adapty SDK only needs to be activated once in your app. ::: To get your **Public SDK Key**: 1. Go to Adapty Dashboard and navigate to [**App settings → General**](https://app.adapty.io/settings/general). 2. From the **Api keys** section, copy the **Public SDK Key** (NOT the Secret Key). 3. Replace `"YOUR_PUBLIC_SDK_KEY"` in the code. :::important - Make sure you use the **Public SDK key** for Adapty initialization, the **Secret key** should be used for [server-side API](getting-started-with-server-side-api) only. - **SDK keys** are unique for every app, so if you have multiple apps make sure you choose the right one. ::: Copy the following code to any app file to activate Adapty: ```typescript showLineNumbers try { await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { // verbose logging is recommended for the development purposes and for the first production release logLevel: 'verbose', // in the development environment, use this variable to avoid multiple activation errors. Set it to your development environment variable __ignoreActivationOnFastRefresh: true, } }); console.log('Adapty activated successfully!'); } catch (error) { console.error('Failed to activate Adapty SDK:', error); } ``` :::tip To avoid activation errors in the development environment, use the [tips](#development-environment-tips). ::: Now set up paywalls in your app: - If you use [Adapty Paywall Builder](adapty-paywall-builder), follow the [Paywall Builder quickstart](capacitor-quickstart-paywalls). - If you build your own paywall UI, see the [quickstart for custom paywalls](capacitor-quickstart-manual). ## Activate AdaptyUI module of Adapty SDK If you plan to use [Paywall Builder](adapty-paywall-builder.md), you need the AdaptyUI module. It is done automatically when you activate the core module; you don't need to do anything else. ## Optional setup ### Logging #### Set up the logging system Adapty logs errors and other important information to help you understand what is going on. There are the following levels available: | Level | Description | | ---------- | ------------------------------------------------------------ | | `error` | Only errors will be logged | | `warn` | Errors and messages from the SDK that do not cause critical errors, but are worth paying attention to will be logged | | `info` | Errors, warnings, and various information messages will be logged | | `verbose` | Any additional information that may be useful during debugging, such as function calls, API queries, etc. will be logged | You can set the log level in your app before or during Adapty configuration: ```typescript showLineNumbers // Set log level before activation adapty.setLogLevel({ logLevel: 'verbose' }); // Or set it during configuration await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { logLevel: 'verbose', } }); ``` ### Data policies Adapty doesn't store personal data of your users unless you explicitly send it, but you can implement additional data security policies to comply with the store or country guidelines. #### Disable IP address collection and sharing When activating the Adapty module, set `ipAddressCollectionDisabled` to `true` to disable user IP address collection and sharing. The default value is `false`. Use this parameter to enhance user privacy, comply with regional data protection regulations (like GDPR or CCPA), or reduce unnecessary data collection when IP-based features aren't required for your app. ```typescript showLineNumbers await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { ipAddressCollectionDisabled: true, } }); ``` #### Disable advertising ID collection and sharing When activating the Adapty module, set `ios.idfaCollectionDisabled` (iOS) or `android.adIdCollectionDisabled` (Android) to `true` to disable the collection of advertising identifiers. The default value is `false`. Use this parameter to comply with App Store/Play Store policies, avoid triggering the App Tracking Transparency prompt, or if your app does not require advertising attribution or analytics based on advertising IDs. ```typescript showLineNumbers await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { ios: { idfaCollectionDisabled: true, }, android: { adIdCollectionDisabled: true, }, } }); ``` #### Set up media cache configuration for AdaptyUI By default, AdaptyUI caches media (such as images and videos) to improve performance and reduce network usage. You can customize the cache settings by providing a custom configuration. Use `mediaCache` to override the default cache settings: ```typescript showLineNumbers await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { mediaCache: { memoryStorageTotalCostLimit: 200 * 1024 * 1024, // Optional: memory cache size in bytes memoryStorageCountLimit: 2147483647, // Optional: max number of items in memory diskStorageSizeLimit: 200 * 1024 * 1024, // Optional: disk cache size in bytes }, } }); ``` Parameters: | Parameter | Required | Description | |-----------|----------|-------------| | memoryStorageTotalCostLimit | optional | Total cache size in memory in bytes. Defaults to platform-specific value. | | memoryStorageCountLimit | optional | The item count limit of the memory storage. Defaults to platform-specific value. | | diskStorageSizeLimit | optional | The file size limit on disk in bytes. Defaults to platform-specific value. | ### Enable local access levels (Android) By default, [local access levels](local-access-levels.md) are enabled on iOS and disabled on Android. To enable them on Android as well, set `localAccessLevelAllowed` to `true`: ```typescript showLineNumbers await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { android: { localAccessLevelAllowed: true, }, } }); ``` ### Clear data on backup restore When `clearDataOnBackup` is set to `true`, the SDK detects when the app is restored from an iCloud backup and deletes all locally stored SDK data, including cached profile information, product details, and paywalls. The SDK then initializes with a clean state. Default value is `false`. :::note Only local SDK cache is deleted. Transaction history with Apple and user data on Adapty servers remain unchanged. ::: ```swift showLineNumbers await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { ios: { clearDataOnBackup: true, }, } }); ``` ## Development environment tips #### Troubleshoot SDK activation errors on Capacitor's live-reload When developing with the Adapty SDK in Capacitor, you may encounter the error: `Adapty can only be activated once. Ensure that the SDK activation call is not made more than once.` This occurs because Capacitor's live-reload feature triggers multiple activation calls during development. To prevent this, use the `__ignoreActivationOnFastRefresh` option set to the Capacitor's development mode flag – it will differ depending on the bundle you are using. ```typescript showLineNumbers try { await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { // Set your development environment variable __ignoreActivationOnFastRefresh: true, } }); } catch (error) { console.error('Failed to activate Adapty SDK:', error); // Handle the error appropriately for your app } ``` ## Troubleshooting #### Minimum iOS version error If you get a minimum iOS version error, update your Podfile: ```diff -platform :ios, min_ios_version_supported +platform :ios, '14.0' # For core features only # OR +platform :ios, '15.0' # If using paywalls created in the paywall builder ``` #### Android backup rules (Auto Backup configuration) Some SDKs (including Adapty) ship their own Android Auto Backup configuration. If you use multiple SDKs that define backup rules, the Android manifest merger can fail with an error mentioning `android:fullBackupContent`, `android:dataExtractionRules`, or `android:allowBackup`. Typical error symptoms: `Manifest merger failed: Attribute application@dataExtractionRules value=(@xml/your_data_extraction_rules) is also present at [com.other.sdk:library:1.0.0] value=(@xml/other_sdk_data_extraction_rules)` :::note These changes should be made in your Android platform directory (typically located in your project's `android/` folder). ::: To resolve this, you need to: - Tell the manifest merger to use your app's values for backup-related attributes. - Create backup rule files that merge Adapty's rules with rules from other SDKs. #### 1. Add the `tools` namespace to your manifest In your `AndroidManifest.xml` file, ensure the root `` tag includes tools: ```xml ... ``` #### 2. Override backup attributes in `` In the same `AndroidManifest.xml` file, update the `` tag so that your app provides the final values and tells the manifest merger to replace library values: ```xml ... ``` If any SDK also sets `android:allowBackup`, include it in `tools:replace` as well: ```xml tools:replace="android:allowBackup,android:fullBackupContent,android:dataExtractionRules" ``` #### 3. Create merged backup rules files Create XML files in your Android project's `res/xml/` directory that combine Adapty's rules with rules from other SDKs. Android uses different backup rule formats depending on the OS version, so creating both files ensures compatibility across all Android versions your app supports. :::note The examples below show AppsFlyer as a sample third-party SDK. Replace or add rules for any other SDKs you're using in your app. ::: **For Android 12 and higher** (uses the new data extraction rules format): ```xml title="sample_data_extraction_rules.xml" ``` **For Android 11 and lower** (uses the legacy full backup content format): ```xml title="sample_backup_rules.xml" :::tip After changing native Android files, run `npx cap sync android` so Capacitor picks up the updated resources if you regenerate the platform. ::: #### Purchases fail after returning from another app in Android If the Activity that starts the purchase flow uses a non-default `launchMode`, Android may recreate or reuse it incorrectly when the user returns from Google Play, a banking app, or a browser. This can cause the purchase result to be lost or treated as canceled. To ensure purchases work correctly, use only `standard` or `singleTop` launch modes for the Activity that starts the purchase flow, and avoid any other modes. In your `AndroidManifest.xml`, ensure the Activity that starts the purchase flow is set to `standard` or `singleTop`: ```xml ``` --- # File: capacitor-quickstart-paywalls --- --- title: "Enable purchases by using paywalls in Capacitor SDK" description: "Learn how to present paywalls in your Capacitor app with Adapty SDK." --- To enable in-app purchases, you need to understand three key concepts: - **Products** – anything users can buy (subscriptions, consumables, lifetime access) - **Paywalls** are configurations that define which products to offer. In Adapty, paywalls are the only way to retrieve products, but this design lets you modify offerings, pricing, and product combinations without touching your app code. - **Placements** – where and when you show paywalls in your app (like `main`, `onboarding`, `settings`). You set up paywalls for placements in the dashboard, then request them by placement ID in your code. This makes it easy to run A/B tests and show different paywalls to different users. Adapty offers you three ways to enable purchases in your app. Select one of them depending on your app requirements: | Implementation | Complexity | When to use | |----------------------------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Adapty Paywall Builder** | ✅ Easy | You [create a complete, purchase-ready paywall in the no-code builder](quickstart-paywalls). Adapty automatically renders it and handles all the complex purchase flow, receipt validation, and subscription management behind the scenes. | | `makePurchase` | 🟡 Medium | You implement your paywall UI in your app code, but still get the paywall object from Adapty to maintain flexibility in product offerings. See the [guide](capacitor-quickstart-manual). | | Observer mode | 🔴 Hard | You implement the purchase flow yourself completely. See the [guide](implement-observer-mode-capacitor). | :::important **The steps below show how to implement a paywall created in the Adapty paywall builder.** If you don't want to use the paywall builder, see the [guide for handling purchases in manually created paywalls](capacitor-making-purchases.md). ::: To display a paywall created in the Adapty paywall builder, in your app code, you only need to: 1. **Get the paywall**: Get the paywall from Adapty. 2. **Display the paywall and Adapty will handle purchases for you**: Show the paywall container you've got in your app. 3. **Handle button actions**: Associate user interactions with the paywall with your app's response to them. For example, open links or close the paywall when users click buttons. ## Before you start Before you start, complete these steps: 1. Connect your app to the [App Store](initial_ios) and/or [Google Play](initial-android) in the Adapty Dashboard. 2. [Create your products](create-product) in Adapty. 3. [Create a paywall and add products to it](create-paywall). 4. [Create a placement and add your paywall to it](create-placement). 5. [Install and activate the Adapty SDK](sdk-installation-capacitor) in your app code. :::tip The fastest way to complete these steps is to follow the [quickstart guide](quickstart). ::: ## 1. Get the paywall Your paywalls are associated with placements configured in the dashboard. Placements allow you to run different paywalls for different audiences or to run [A/B tests](ab-tests.md). To get a paywall created in the Adapty paywall builder, you need to: 1. Get the `paywall` object by the [placement](placements.md) ID using the `getPaywall` method and check whether it is a paywall created in the builder using the `hasViewConfiguration` property. 2. Create the paywall view using the `createPaywallView` method. The view contains the UI elements and styling needed to display the paywall. :::important To get the view configuration, you must switch on the **Show on device** toggle in the Paywall Builder. Otherwise, you will get an empty view configuration, and the paywall won't be displayed. ::: ```typescript showLineNumbers title="Capacitor" try { const paywall = await adapty.getPaywall({ placementId: 'YOUR_PLACEMENT_ID', }); // the requested paywall } catch (error) { // handle the error } if (paywall.hasViewConfiguration) { try { const view = await createPaywallView(paywall); } catch (error) { // handle the error } } else { // use your custom logic } ``` ## 2. Display the paywall Now, when you have the paywall configuration, it's enough to add a few lines to display your paywall. To display the paywall, use the `view.present()` method on the `view` created by the `createPaywallView` method. Each `view` can only be used once. If you need to display the paywall again, call `createPaywallView` one more to create a new `view` instance. ```typescript showLineNumbers title="Capacitor" try { await view.present(); } catch (error) { // handle the error } ``` :::tip For more details on how to display a paywall, see our [guide](capacitor-present-paywalls.md). ::: ## 3. Handle button actions When users click buttons in the paywall, the Capacitor SDK automatically handles purchases, restoration, and closing the paywall. However, other buttons have custom or pre-defined IDs and require handling actions in your code. Or, you may want to override their default behavior. For example, you may want to keep the paywall open after your app users open a web link. Let's see how you can handle it in your implementation. :::tip Read our guides on how to handle button [actions](capacitor-handle-paywall-actions.md) and [events](capacitor-handling-events.md). ::: ```typescript showLineNumbers title="Capacitor" const unsubscribe = view.setEventHandlers({ onUrlPress(url) { window.open(url, '_blank'); return false; }, }); ``` ## Next steps Your paywall is ready to be displayed in the app. Test your purchases in the [App Store sandbox](test-purchases-in-sandbox) or in [Google Play Store](testing-on-android) to make sure you can complete a test purchase from the paywall. Now, you need to [check the users' access level](capacitor-check-subscription-status.md) to ensure you display a paywall or give access to paid features to right users. ## Full example Here is how all those steps can be integrated in your app together. ```typescript showLineNumbers title="Capacitor" export default function PaywallScreen() { const showPaywall = async () => { try { const paywall = await adapty.getPaywall({ placementId: 'YOUR_PLACEMENT_ID', }); if (!paywall.hasViewConfiguration) { // use your custom logic return; } const view = await createPaywallView(paywall); view.setEventHandlers({ onUrlPress(url) { window.open(url, '_blank'); return false; }, }); await view.present(); } catch (error) { // handle any error that may occur during the process console.warn('Error showing paywall:', error); } }; return (
); } ``` --- # File: capacitor-check-subscription-status --- --- title: "Check subscription status in Capacitor SDK" description: "Learn how to check subscription status in your Capacitor app with Adapty." --- To decide whether users can access paid content or see a paywall, you need to check their [access level](access-level.md) in the profile. This article shows you how to access the profile state to decide what users need to see - whether to show them a paywall or grant access to paid features. ## Get subscription status When you decide whether to show a paywall or paid content to a user, you check their [access level](access-level.md) in their profile. You have two options: - Call `getProfile` if you need the latest profile data immediately (like on app launch) or want to force an update. - Set up **automatic profile updates** to keep a local copy that's automatically refreshed whenever the subscription status changes. ### Get profile The easiest way to get the subscription status is to use the `getProfile` method to access the profile: ```typescript showLineNumbers try { const profile = await adapty.getProfile(); } catch (error) { // handle the error } ``` ### Listen to subscription updates To automatically receive profile updates in your app: 1. Use `adapty.addListener('onLatestProfileLoad')` to listen for profile changes - Adapty will automatically call this method whenever the user's subscription status changes. 2. Store the updated profile data when this method is called, so you can use it throughout your app without making additional network requests. ```typescript showLineNumbers class SubscriptionManager { private currentProfile: any = null; constructor() { // Listen for profile updates adapty.addListener('onLatestProfileLoad', (data) => { this.currentProfile = data.profile; // Update UI, unlock content, etc. }); } // Use stored profile instead of calling getProfile() hasAccess(): boolean { return this.currentProfile?.accessLevels?.['YOUR_ACCESS_LEVEL']?.isActive ?? false; } } ``` :::note Adapty automatically calls the `onLatestProfileLoad` event listener when your app starts, providing cached subscription data even if the device is offline. ::: ## Connect profile with paywall logic When you need to make immediate decisions about showing paywalls or granting access to paid features, you can check the user's profile directly. This approach is useful for scenarios like app launch, when entering premium sections, or before displaying specific content. ```typescript showLineNumbers const checkAccessLevel = async () => { try { const profile = await adapty.getProfile(); return profile?.accessLevels?.['YOUR_ACCESS_LEVEL']?.isActive === true; } catch (error) { console.warn('Error checking access level:', error); return false; // Show paywall if access check fails } }; const getAccessLevel = () => { return profile?.accessLevels?.['YOUR_ACCESS_LEVEL']; }; const initializePaywall = async () => { try { await loadPaywall(); const hasAccess = await checkAccessLevel(); if (!hasAccess) { // Show paywall if no access } } catch (error) { console.warn('Error initializing paywall:', error); } }; ``` ## Next steps Now, when you know how to track the subscription status, learn how to [work with user profiles](capacitor-quickstart-identify.md) to ensure they can access what they have paid for. --- # File: capacitor-quickstart-identify --- --- title: "Identify users in Capacitor SDK" description: "Quickstart guide to setting up Adapty for in-app subscription management in Capacitor." --- How you manage users' purchases depends on your app's authentication model: - If your app doesn't use backend authentication and doesn't store user data, see the [section about anonymous users](#anonymous-users). - If your app has (or will have) backend authentication, see the [section about identified users](#identified-users). :::tip **Key concepts**: - **Profiles** are the entities required for the SDK to work. Adapty creates them automatically. They can be anonymous (without customer user ID) or identified (with customer user ID). - **Customer user IDs** are optional identifiers **you create** for Adapty to link your users to their Adapty profiles. ::: Here is what is different for anonymous and identified users: | | Anonymous users | Identified users | |-------------------------|------------------------------------------------------|-------------------------------------------------------------------------| | **Purchase management** | Store-level purchase restoration | Maintain purchase history across devices through their customer user ID | | **Profile management** | New profiles on each reinstall | The same profile across sessions and devices | | **Data persistence** | Anonymous users' data is tied to device/installation | Identified users' data persists across devices and sessions | ## Anonymous users If you don't have backend authentication, **you don't need to handle authentication in the app code**: 1. When the SDK is activated on the app's first launch, Adapty **creates a new profile for the user**. 2. When the user purchases anything in the app, this purchase is **associated with their Adapty profile and their store account**. 3. When the user **re-installs** the app or installs it from a **new device**, Adapty **creates a new empty profile on activation**. 4. If the user has previously made purchases in your app, by default, their purchases are automatically synced from the App Store on the SDK activation. :::note Backup restores behave differently from reinstalls. By default, when a user restores from a backup, the SDK preserves cached data and does not create a new profile. You can configure this behavior using the `clearDataOnBackup` setting. [Learn more](sdk-installation-capacitor#clear-data-on-backup-restore). ::: ## Identified users - If a profile doesn't have a customer user ID yet (meaning, **the user isn't signed in**), when you send a customer user ID, it gets associated with that profile. - If it is a **re-installation, signing in, or installation from a new device**, and you have sent their customer user ID before, a new profile is not created. Instead, we switch to the existing profile associated with the customer user ID. You have two options to identify users in the app: - [**During login/signup:**](#during-loginsignup) If users sign in after your app starts, call `identify()` with a customer user ID when they authenticate. - [**During the SDK activation:**](#during-the-sdk-activation) If you already have a customer user ID stored when the app launches, send it when calling `activate()`. :::important By default, when Adapty receives a purchase from a Customer User ID that is currently associated with another Customer User ID, the access level is shared, so both profiles have paid access. You can configure this setting to transfer paid access from one profile to another or disable sharing completely. See the [article](general#6-sharing-purchases-between-user-accounts) for more details. ::: ### During login/signup If you're identifying users after the app launch (for example, after they log into your app or sign up), use the `identify` method to set their customer user ID. - If you **haven't used this customer user ID before**, Adapty will automatically link it to the current profile. - If you **have used this customer user ID to identify the user before**, Adapty will switch to working with the profile associated with this customer user ID. :::tip When creating a customer user ID, save it with your user data so you can send the same ID when they log in from new devices or reinstall your app. ::: ```typescript showLineNumbers try { await adapty.identify({ customerUserId: "YOUR_USER_ID" }); // successfully identified } catch (error) { // handle the error } ``` ### During the SDK activation If you already know a customer user ID when you activate the SDK, you can send it in the `activate` method instead of calling `identify` separately. If you know a customer user ID but set it only after the activation, that will mean that, upon activation, Adapty will create a new empty profile and switch to the existing one only after you call `identify`. You can pass either an existing customer user ID (the one you have used before) or a new one. If you pass a new one, a new profile created on the activation will be automatically linked to the customer user ID. :::tip To exclude created empty profiles from the dashboard [analytics](analytics-charts.md), go to **App settings** and set up [**Installs definition for analytics**](general#4-installs-definition-for-analytics). ::: ```typescript showLineNumbers await adapty.activate({ apiKey: "YOUR_PUBLIC_SDK_KEY", params: { customerUserId: "YOUR_USER_ID" } }); ``` ### Log users out If you have a button for logging users out, use the `logout` method. This creates a new anonymous profile ID for the user. ```typescript showLineNumbers try { await adapty.logout(); // successful logout } catch (error) { // handle the error } ``` :::info To log users back into the app, use the `identify` method. ::: ### Allow purchases without login If your users can make purchases both before and after they log into your app, you don't need to do additional setup: Here's how it works: 1. When a logged-out user makes a purchase, Adapty ties it to their anonymous profile ID. 2. When the user logs into their account, Adapty switches to working with their identified profile. - If it is an existing customer user ID (the customer user ID is already linked to a profile), Adapty syncs its transactions automatically. - If it is a new customer user ID (e.g., the purchase has been made before registration), Adapty assigns the customer user ID to the current profile, so all the purchase history is maintained. --- # File: adapty-cursor-capacitor --- --- title: "Integrate Adapty into your Capacitor app with AI assistance" description: "A step-by-step guide to integrating Adapty into your Capacitor app using Cursor, Context7, ChatGPT, Claude, or other AI tools." --- This guide helps you integrate Adapty into your Capacitor app with the help of an LLM. You'll start by preparing your Adapty dashboard, then work through each implementation stage by sending focused doc links to your LLM. At the end, you'll find best practices for setting up your AI tools with Adapty documentation. :::tip Copy this entire page as Markdown and paste it into your LLM to get started — click **Copy for LLM** at the top of the page or open [the .md version](https://adapty.io/docs/adapty-cursor-capacitor.md). The LLM will use the guide links and checkpoints to walk you through each stage. ::: ## Before you start: dashboard checklist Adapty requires dashboard configuration before you write any SDK code. Your LLM cannot look up dashboard values for you — you'll need to provide them. ### Required before coding 1. **Connect your app stores**: In the Adapty Dashboard, go to **App settings → General**. Connect both App Store and Google Play if your Capacitor app targets both platforms. This is required for purchases to work. [Connect app stores](integrate-payments.md) 2. **Copy your Public SDK key**: In the Adapty Dashboard, go to **App settings → General**, then find the **API keys** section. In code, this is the string you pass to `adapty.activate()`. 3. **Create at least one product**: In the Adapty Dashboard, go to the **Products** page. You don't reference products directly in code — Adapty delivers them through paywalls. [Add products](quickstart-products.md) 4. **Create a paywall and a placement**: In the Adapty Dashboard, create a paywall on the **Paywalls** page, then assign it to a placement on the **Placements** page. In code, the placement ID is the string you pass to `adapty.getPaywall()`. [Create paywall](quickstart-paywalls.md) 5. **Set up access levels**: In the Adapty Dashboard, configure per product on the **Products** page. In code, the string checked in `profile.accessLevels['premium']?.isActive`. The default `premium` access level works for most apps. If paying users get access to different features depending on the product (for example, a `basic` plan vs. a `pro` plan), [create additional access levels](assigning-access-level-to-a-product.md) before you start coding. :::tip Once you have all five, you're ready to write code. Tell your LLM: "My Public SDK key is X, my placement ID is Y" so it can generate correct initialization and paywall-fetching code. ::: ### Set up when ready These are not required to start coding, but you'll want them as your integration matures: - **A/B tests**: Configure on the **Placements** page. No code change needed. [A/B tests](ab-tests.md) - **Additional paywalls and placements**: Add more `getPaywall` calls with different placement IDs. - **Analytics integrations**: Configure on the **Integrations** page. Setup varies by integration. See [analytics integrations](analytics-integration.md) and [attribution integrations](attribution-integration.md). ## Feed Adapty docs to your LLM ### Use Context7 (recommended) [Context7](https://context7.com) is an MCP server that gives your LLM direct access to up-to-date Adapty documentation. Your LLM fetches the right docs automatically based on what you ask — no manual URL pasting needed. Context7 works with **Cursor**, **Claude Code**, **Windsurf**, and other MCP-compatible tools. To set it up, run: ``` npx ctx7 setup ``` This detects your editor and configures the Context7 server. For manual setup, see the [Context7 GitHub repository](https://github.com/upstash/context7). Once configured, reference the Adapty library in your prompts: ``` Use the adaptyteam/adapty-docs library to look up how to install the Capacitor SDK ``` :::warning Even though Context7 removes the need to paste doc links manually, the implementation order matters. Follow the [implementation walkthrough](#implementation-walkthrough) below step by step to make sure everything works. ::: ### Use plain text docs You can access any Adapty doc as plain text Markdown. Add `.md` to the end of its URL, or click **Copy for LLM** under the article title. For example: [adapty-cursor-capacitor.md](https://adapty.io/docs/adapty-cursor-capacitor.md). Each stage in the [implementation walkthrough](#implementation-walkthrough) below includes a "Send this to your LLM" block with `.md` links to paste. For more documentation at once, see [index files and platform-specific subsets](#plain-text-doc-index-files) below. ## Implementation walkthrough The rest of this guide walks through Adapty integration in implementation order. Each stage includes the docs to send to your LLM, what you should see when done, and common issues. ### Plan your integration Before jumping into code, ask your LLM to analyze your project and create an implementation plan. If your AI tool supports a planning mode (like Cursor's or Claude Code's plan mode), use it so the LLM can read both your project structure and the Adapty docs before writing any code. Tell your LLM which approach you use for purchases — this affects the guides it should follow: - [**Adapty Paywall Builder**](adapty-paywall-builder.md): You create paywalls in Adapty's no-code builder, and the SDK renders them automatically. - [**Manually created paywalls**](capacitor-making-purchases.md): You build your own paywall UI in code but still use Adapty to fetch products and handle purchases. - [**Observer mode**](observer-vs-full-mode.md): You keep your existing purchase infrastructure and use Adapty only for analytics and integrations. Not sure which one to pick? Read the [comparison table in the quickstart](capacitor-quickstart-paywalls.md). ### Install and configure the SDK Add the Adapty SDK dependency using npm and activate it with your Public SDK key. This is the foundation — nothing else works without it. **Guide:** [Install & configure Adapty SDK](sdk-installation-capacitor.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/sdk-installation-capacitor.md ``` :::tip[Checkpoint] - **Expected:** App builds and runs on both iOS and Android. Console shows Adapty activation log. - **Gotcha:** "Public API key is missing" → check you replaced the placeholder with your real key from App settings. ::: ### Show paywalls and handle purchases Fetch a paywall by placement ID, display it, and handle purchase events. The guides you need depend on how you handle purchases. Test each purchase in the sandbox as you go — don't wait until the end. See [Test purchases in sandbox](test-purchases-in-sandbox.md) for setup instructions. **Guides:** - [Enable purchases using paywalls (quickstart)](capacitor-quickstart-paywalls.md) - [Fetch Paywall Builder paywalls and their configuration](capacitor-get-pb-paywalls.md) - [Display paywalls](capacitor-present-paywalls.md) - [Handle paywall events](capacitor-handling-events.md) - [Respond to button actions](capacitor-handle-paywall-actions.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/capacitor-quickstart-paywalls.md - https://adapty.io/docs/capacitor-get-pb-paywalls.md - https://adapty.io/docs/capacitor-present-paywalls.md - https://adapty.io/docs/capacitor-handling-events.md - https://adapty.io/docs/capacitor-handle-paywall-actions.md ``` :::tip[Checkpoint] - **Expected:** Paywall appears with your configured products. Tapping a product triggers the sandbox purchase dialog. - **Gotcha:** Empty paywall or `getPaywall` error → verify placement ID matches the dashboard exactly and the placement has an audience assigned. ::: **Guides:** - [Enable purchases in your custom paywall (quickstart)](capacitor-quickstart-manual.md) - [Fetch paywalls and products](fetch-paywalls-and-products-capacitor.md) - [Render paywall designed by remote config](present-remote-config-paywalls-capacitor.md) - [Make purchases](capacitor-making-purchases.md) - [Restore purchases](capacitor-restore-purchase.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/capacitor-quickstart-manual.md - https://adapty.io/docs/fetch-paywalls-and-products-capacitor.md - https://adapty.io/docs/present-remote-config-paywalls-capacitor.md - https://adapty.io/docs/capacitor-making-purchases.md - https://adapty.io/docs/capacitor-restore-purchase.md ``` :::tip[Checkpoint] - **Expected:** Your custom paywall displays products fetched from Adapty. Tapping a product triggers the sandbox purchase dialog. - **Gotcha:** Empty products array → verify the paywall has products assigned in the dashboard and the placement has an audience. ::: **Guides:** - [Observer mode overview](observer-vs-full-mode.md) - [Implement Observer mode](implement-observer-mode-capacitor.md) - [Report transactions in Observer mode](report-transactions-observer-mode-capacitor.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/observer-vs-full-mode.md - https://adapty.io/docs/implement-observer-mode-capacitor.md - https://adapty.io/docs/report-transactions-observer-mode-capacitor.md ``` :::tip[Checkpoint] - **Expected:** After a sandbox purchase using your existing purchase flow, the transaction appears in the Adapty dashboard **Event Feed**. - **Gotcha:** No events → verify you're reporting transactions to Adapty and server notifications are configured for both stores. ::: ### Check subscription status After a purchase, check the user profile for an active access level to gate premium content. **Guide:** [Check subscription status](capacitor-check-subscription-status.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/capacitor-check-subscription-status.md ``` :::tip[Checkpoint] - **Expected:** After a sandbox purchase, `profile.accessLevels['premium']?.isActive` returns `true`. - **Gotcha:** Empty `accessLevels` after purchase → check the product has an access level assigned in the dashboard. ::: ### Identify users Link your app user accounts to Adapty profiles so purchases persist across devices. :::important Skip this step if your app has no authentication. ::: **Guide:** [Identify users](capacitor-quickstart-identify.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/capacitor-quickstart-identify.md ``` :::tip[Checkpoint] - **Expected:** After calling `adapty.identify()`, the dashboard **Profiles** section shows your custom user ID. - **Gotcha:** Call `identify` after activation but before fetching paywalls to avoid anonymous profile attribution. ::: ### Prepare for release Once your integration works in the sandbox, walk through the release checklist to make sure everything is production-ready. **Guide:** [Release checklist](release-checklist.md) Send this to your LLM: ``` Read these Adapty docs before releasing: - https://adapty.io/docs/release-checklist.md ``` :::tip[Checkpoint] - **Expected:** All checklist items confirmed: store connections, server notifications, purchase flow, access level checks, and privacy requirements. - **Gotcha:** Missing server notifications → configure App Store Server Notifications in **App settings → iOS SDK** and Google Play Real-Time Developer Notifications in **App settings → Android SDK**. ::: ## Plain text doc index files If you need to give your LLM broader context beyond individual pages, we host index files that list or combine all Adapty documentation: - [`llms.txt`](https://adapty.io/docs/llms.txt): Lists all pages with `.md` links. An [emerging standard](https://llmstxt.org/) for making websites accessible to LLMs. Note that for some AI agents (e.g., ChatGPT) you will need to download `llms.txt` and upload it to the chat as a file. - [`llms-full.txt`](https://adapty.io/docs/llms-full.txt): The entire Adapty documentation site combined into a single file. Very large — use only when you need the full picture. - Capacitor-specific [`capacitor-llms.txt`](https://adapty.io/docs/capacitor-llms.txt) and [`capacitor-llms-full.txt`](https://adapty.io/docs/capacitor-llms-full.txt): Platform-specific subsets that save tokens compared to the full site. --- # File: capacitor-paywalls --- --- title: "Paywalls" description: "Learn how to work with paywalls in your Capacitor app with Adapty SDK." --- ## Display paywalls ### Adapty Paywall Builder :::tip To get started with the Adapty Paywall Builder paywalls quickly, see our [quickstart guide](capacitor-quickstart-paywalls). ::: ### Implement paywalls manually For more guides on implementing paywalls and handling purchases manually, see the [category](capacitor-implement-paywalls-manually). ## Useful features --- # File: capacitor-get-pb-paywalls --- --- title: "Fetch Paywall Builder paywalls and their configuration in Capacitor SDK" description: "Learn how to retrieve PB paywalls in Adapty for better subscription control in your Capacitor app." --- After [you designed the visual part for your paywall](adapty-paywall-builder) with the new Paywall Builder in the Adapty Dashboard, you can display it in your mobile app. The first step in this process is to get the paywall associated with the placement and its view configuration as described below. Please be aware that this topic refers to Paywall Builder-customized paywalls. For guidance on fetching remote config paywalls, please refer to the [Fetch paywalls and products for remote config paywalls in your mobile app](fetch-paywalls-and-products-capacitor) topic.
Before you start displaying paywalls in your mobile app (click to expand) 1. [Create your products](create-product) in the Adapty Dashboard. 2. [Create a paywall and incorporate the products into it](create-paywall) in the Adapty Dashboard. 3. [Create placements and incorporate your paywall into it](create-placement) in the Adapty Dashboard. 4. Install [Adapty SDK](sdk-installation-capacitor.md) in your mobile app.
## Fetch paywall designed with Paywall Builder If you've [designed a paywall using the Paywall Builder](adapty-paywall-builder), you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. Nevertheless, you need to get its ID via the placement, its view configuration, and then present it in your mobile app. To ensure optimal performance, it's crucial to retrieve the paywall and its [view configuration](capacitor-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) as early as possible, allowing sufficient time for images to download before presenting them to the user. To get a paywall, use the `getPaywall` method: ```typescript showLineNumbers try { const paywall = await adapty.getPaywall({ placementId: 'YOUR_PLACEMENT_ID', locale: 'en', }); // the requested paywall } catch (error) { // handle the error } ``` Parameters: | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the [paywall localization](add-paywall-locale-in-adapty-paywall-builder). This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.

| | **params** | optional | Additional parameters for fetching the paywall. | **Don't hardcode product IDs.** The only ID you should hardcode is the placement ID. Paywalls are configured remotely, so the number of products and available offers can change at any time. Your app must handle these changes dynamically—if a paywall returns two products today and three tomorrow, display all of them without code changes. Response parameters: | Parameter | Description | | :-------- |:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Paywall | An [`AdaptyPaywall`](https://capacitor.adapty.io/interfaces/adaptypaywall) object with a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch the view configuration of paywall designed using Paywall Builder :::important Make sure to enable the **Show on device** toggle in the paywall builder. If this option isn't turned on, the view configuration won't be available to retrieve. ::: After fetching the paywall, check if it includes a `ViewConfiguration`, which indicates that it was created using Paywall Builder. This will guide you on how to display the paywall. If the `ViewConfiguration` is present, treat it as a Paywall Builder paywall; if not, [handle it as a remote config paywall](present-remote-config-paywalls-capacitor). In Capacitor SDK, directly call the `createPaywallView` method without manually fetching the view configuration first. :::warning The result of the `createPaywallView` method can only be used once. If you need to use it again, call the `createPaywallView` method anew. ::: ```typescript showLineNumbers if (paywall.hasViewConfiguration) { try { const view = await createPaywallView(paywall); } catch (error) { // handle the error } } else { // use your custom logic } ``` Parameters: | Parameter | Presence | Description | | :------------------- | :------- | :----------------------------------------------------------- | | **paywall** | required | An `AdaptyPaywall` object to obtain a controller for the desired paywall. | | **customTags** | optional | Define a dictionary of custom tags and their resolved values. Custom tags serve as placeholders in the paywall content, dynamically replaced with specific strings for personalized content within the paywall. Refer to [Custom tags in paywall builder](custom-tags-in-paywall-builder) topic for more details. | | **prefetchProducts** | optional | Enable to optimize the display timing of products on the screen. When `true` AdaptyUI will automatically fetch the necessary products. Default: `false`. | :::note If you are using multiple languages, learn how to add a [Paywall Builder localization](add-paywall-locale-in-adapty-paywall-builder) and how to use locale codes correctly [here](capacitor-localizations-and-locale-codes). ::: Once you have the view, [present the paywall](capacitor-present-paywalls). ## Get a paywall for a default audience to fetch it faster Typically, paywalls are fetched almost instantly, so you don't need to worry about speeding up this process. However, in cases where you have numerous audiences and paywalls, and your users have a weak internet connection, fetching a paywall may take longer than you'd like. In such situations, you might want to display a default paywall to ensure a smooth user experience rather than showing no paywall at all. To address this, you can use the `getPaywallForDefaultAudience` method, which fetches the paywall of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the paywall by the `getPaywall` method, as detailed in the [Fetch Paywall Information](#fetch-paywall-designed-with-paywall-builder) section above. :::warning Why we recommend using `getPaywall` The `getPaywallForDefaultAudience` method comes with a few significant drawbacks: - **Potential backward compatibility issues**: If you need to show different paywalls for different app versions (current and future), you'll either have to design paywalls that support the current (legacy) version or accept that users with the current (legacy) version might encounter issues with non-rendered paywalls. - **Loss of targeting**: All users will see the same paywall designed for the **All Users** audience, which means you lose personalized targeting (including based on countries, marketing attribution or your own custom attributes). If you're willing to accept these drawbacks to benefit from faster paywall fetching, use the `getPaywallForDefaultAudience` method as follows. Otherwise stick to `getPaywall` described [above](#fetch-paywall-designed-with-paywall-builder). ::: ```typescript showLineNumbers try { const paywall = await adapty.getPaywallForDefaultAudience({ placementId: 'YOUR_PLACEMENT_ID', locale: 'en', }); // the requested paywall } catch (error) { // handle the error } ``` :::note The `getPaywallForDefaultAudience` method is available starting from Capacitor SDK version 2.11.2. ::: | Parameter | Presence | Description | |---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | required | The identifier of the [Placement](placements). This is the value you specified when creating a placement in your Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

See [Localizations and locale codes](capacitor-localizations-and-locale-codes) for more information on locale codes and how we recommend using them.

| | **params** | optional | Additional parameters for fetching the paywall. | ## Customize assets To customize images and videos in your paywall, implement the custom assets. Hero images and videos have predefined IDs: `hero_image` and `hero_video`. In a custom asset bundle, you target these elements by their IDs and customize their behavior. For other images and videos, you need to [set a custom ID](https://adapty.io/docs/custom-media) in the Adapty dashboard. For example, you can: - Show a different image or video to some users. - Show a local preview image while a remote main image is loading. - Show a preview image before running a video. :::important To use this feature, update the Adapty Capacitor SDK to version 3.8.0 or higher. ::: Here's an example of how you can provide custom asssets via a simple dictionary: ```typescript showLineNumbers const customAssets: Record = { 'custom_image': { type: 'image', relativeAssetPath: 'custom_image.png' }, 'hero_video': { type: 'video', fileLocation: { ios: { fileName: 'custom_video.mp4' }, android: { relativeAssetPath: 'videos/custom_video.mp4' } } } }; view = await createPaywallView(paywall, { customAssets }); ``` :::note If an asset is not found, the paywall will fall back to its default appearance. ::: --- # File: capacitor-present-paywalls --- --- title: "Present Paywall Builder paywalls in Capacitor SDK" description: "Present paywalls in Capacitor apps using Adapty." --- If you've customized a paywall using the Paywall Builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. :::warning This guide is for **Paywall Builder paywalls** only. The process for presenting paywalls differs for remote config paywalls. For presenting **remote config paywalls**, see [Render paywall designed by remote config](present-remote-config-paywalls). ::: To display a paywall, use the `view.present()` method on the `view` created by the [`createPaywallView`](capacitor-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) method. Each `view` can only be used once. If you need to display the paywall again, call `createPaywallView` one more to create a new `view` instance. :::warning Reusing the same `view` without recreating it may result in an error. ::: ```typescript showLineNumbers const view = await createPaywallView(paywall); view.setEventHandlers({ onUrlPress(url) { window.open(url, '_blank'); return false; }, }); try { await view.present(); } catch (error) { // handle the error } ``` ## Use developer-defined timer To use developer-defined timers in your mobile app, use the `timerId`, in this example, `CUSTOM_TIMER_NY`, the **Timer ID** of the developer-defined timer you set in the Adapty dashboard. It ensures your app dynamically updates the timer with the correct value—like `13d 09h 03m 34s` (calculated as the timer's end time, such as New Year's Day, minus the current time). ```typescript showLineNumbers const customTimers = { 'CUSTOM_TIMER_NY': new Date(2025, 0, 1) }; const view = await createPaywallView(paywall, { customTimers }); ``` In this example, `CUSTOM_TIMER_NY` is the **Timer ID** of the developer-defined timer you set in the Adapty dashboard. The timer ensures your app dynamically updates the timer with the correct value—like `13d 09h 03m 34s` (calculated as the timer's end time, such as New Year's Day, minus the current time). ## Show dialog Use this method instead of native alert dialogs when a paywall view is presented on Android. On Android, regular alerts appear behind the paywall view, which makes them invisible to users. This method ensures proper dialog presentation above the paywall on all platforms. ```typescript showLineNumbers title="Capacitor" try { const action = await view.showDialog({ title: 'Close paywall?', content: 'You will lose access to exclusive offers.', primaryActionTitle: 'Stay', secondaryActionTitle: 'Close', }); if (action === 'secondary') { // User confirmed - close the paywall await view.dismiss(); } // If primary - do nothing, user stays } catch (error) { // handle error } ``` ## Configure iOS presentation style Configure how the paywall is presented on iOS by passing the `iosPresentationStyle` parameter to the `present()` method. The parameter accepts `'full_screen'` (default) or `'page_sheet'` values. ```typescript showLineNumbers await view.present({ iosPresentationStyle: 'page_sheet' }); ``` --- # File: capacitor-handle-paywall-actions --- --- title: "Respond to button actions in Capacitor SDK" description: "Handle paywall button actions in Capacitor using Adapty for better app monetization." --- If you are building paywalls using the Adapty paywall builder, it's crucial to set up buttons properly: 1. Add a [button in the paywall builder](paywall-buttons.md) and assign it either a pre-existing action or create a custom action ID. 2. Write code in your app to handle each action you've assigned. This guide shows how to handle custom and pre-existing actions in your code. ## Close paywalls To add a button that will close your paywall: 1. In the paywall builder, add a button and assign it the **Close** action. 2. In your app code, implement a handler for the `close` action that dismisses the paywall. :::info In the Capacitor SDK, the `close` action triggers closing the paywall by default. However, you can override this behavior in your code if needed. For example, closing one paywall might trigger opening another. ::: ```typescript showLineNumbers const view = await createPaywallView(paywall); const unsubscribe = view.setEventHandlers({ onCloseButtonPress() { console.log('User closed paywall'); return true; // Allow the paywall to close } }); ``` ## Open URLs from paywalls :::tip If you want to add a group of links (e.g., terms of use and purchase restoration), add a **Link** element in the paywall builder and handle it the same way as buttons with the **Open URL** action. ::: To add a button that opens a link from your paywall (e.g., **Terms of use** or **Privacy policy**): 1. In the paywall builder, add a button, assign it the **Open URL** action, and enter the URL you want to open. 2. In your app code, implement a handler for the `openUrl` action that opens the received URL in a browser. :::info In the Capacitor SDK, the `window.open` action triggers opening the URL by default. However, you can override this behavior in your code if needed. ::: ```typescript showLineNumbers const view = await createPaywallView(paywall); const unsubscribe = view.setEventHandlers({ onUrlPress(url) { window.open(url, '_blank'); return false; // Don't close the paywall }, }); ``` ## Log into the app To add a button that logs users into your app: 1. In the paywall builder, add a button and assign it the **Login** action. 2. In your app code, implement a handler for the `login` action that identifies your user. ```typescript showLineNumbers const view = await createPaywallView(paywall); const unsubscribe = view.setEventHandlers({ onCustomAction(actionId) { if (actionId === 'login') { // Navigate to login screen console.log('User requested login'); } } }); ``` ## Handle custom actions To add a button that handles any other actions: 1. In the paywall builder, add a button, assign it the **Custom** action, and assign it an ID. 2. In your app code, implement a handler for the action ID you've created. For example, if you have another set of subscription offers or one-time purchases, you can add a button that will display another paywall: ```typescript showLineNumbers const unsubscribe = view.setEventHandlers({ onCustomAction(actionId) { if (actionId === 'openNewPaywall') { // Display another paywall console.log('User requested new paywall'); } }, }); ``` --- # File: capacitor-handling-events --- --- title: "Capacitor - Handle paywall events" description: "Handle subscription events in Capacitor with Adapty's SDK." --- :::important This guide covers event handling for purchases, restorations, product selection, and paywall rendering. You must also implement button handling (closing paywall, opening links, etc.). See our [guide on handling button actions](capacitor-handle-paywall-actions.md) for details. ::: Paywalls configured with the [Paywall Builder](adapty-paywall-builder) don't need extra code to make and restore purchases. However, they generate some events that your app can respond to. Those events include button presses (close buttons, URLs, product selections, and so on) as well as notifications on purchase-related actions taken on the paywall. Learn how to respond to these events below. To control or monitor processes occurring on the paywall screen within your mobile app, implement the `view.setEventHandlers` method: ```typescript showLineNumbers const view = await createPaywallView(paywall); const unsubscribe = view.setEventHandlers({ onCloseButtonPress() { console.log('User closed paywall'); return true; // Allow the paywall to close }, onAndroidSystemBack() { console.log('User pressed back button'); return true; // Allow the paywall to close }, onAppeared() { console.log('Paywall appeared'); return false; // Don't close the paywall }, onDisappeared() { console.log('Paywall disappeared'); }, onPurchaseCompleted(purchaseResult, product) { console.log('Purchase completed:', purchaseResult); return purchaseResult.type !== 'user_cancelled'; // Close if not cancelled }, onPurchaseStarted(product) { console.log('Purchase started:', product); return false; // Don't close the paywall }, onPurchaseFailed(error, product) { console.error('Purchase failed:', error); return false; // Don't close the paywall }, onRestoreCompleted(profile) { console.log('Restore completed:', profile); return true; // Close the paywall after successful restore }, onRestoreFailed(error) { console.error('Restore failed:', error); return false; // Don't close the paywall }, onProductSelected(productId) { console.log('Product selected:', productId); return false; // Don't close the paywall }, onRenderingFailed(error) { console.error('Rendering failed:', error); return false; // Don't close the paywall }, onLoadingProductsFailed(error) { console.error('Loading products failed:', error); return false; // Don't close the paywall }, onUrlPress(url) { window.open(url, '_blank'); return false; // Don't close the paywall }, }); ```
Event examples (Click to expand) ```typescript // onCloseButtonPress { "event": "close_button_press" } // onAndroidSystemBack { "event": "android_system_back" } // onAppeared { "event": "paywall_shown" } // onDisappeared { "event": "paywall_closed" } // onUrlPress { "event": "url_press", "url": "https://example.com/terms" } // onCustomAction { "event": "custom_action", "actionId": "login" } // onProductSelected { "event": "product_selected", "productId": "premium_monthly" } // onPurchaseStarted { "event": "purchase_started", "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } // onPurchaseCompleted - Success { "event": "purchase_completed", "purchaseResult": { "type": "success", "profile": { "accessLevels": { "premium": { "id": "premium", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } } } }, "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } // onPurchaseCompleted - Cancelled { "event": "purchase_completed", "purchaseResult": { "type": "user_cancelled" }, "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } // onPurchaseFailed { "event": "purchase_failed", "error": { "code": "purchase_failed", "message": "Purchase failed due to insufficient funds", "details": { "underlyingError": "Insufficient funds in account" } } } // onRestoreCompleted { "event": "restore_completed", "profile": { "accessLevels": { "premium": { "id": "premium", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } }, "subscriptions": [ { "vendorProductId": "premium_monthly", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } ] } } // onRestoreFailed { "event": "restore_failed", "error": { "code": "restore_failed", "message": "Purchase restoration failed", "details": { "underlyingError": "No previous purchases found" } } } // onRenderingFailed { "event": "rendering_failed", "error": { "code": "rendering_failed", "message": "Failed to render paywall interface", "details": { "underlyingError": "Invalid paywall configuration" } } } // onLoadingProductsFailed { "event": "loading_products_failed", "error": { "code": "products_loading_failed", "message": "Failed to load products from the server", "details": { "underlyingError": "Network timeout" } } } ```
You can register event handlers you need, and miss those you do not need. In this case, unused event listeners would not be created. There are no required event handlers. Event handlers return a boolean. If `true` is returned, the displaying process is considered complete, thus the paywall screen closes and event listeners for this view are removed. Some event handlers have a default behavior that you can override if needed: - `onCloseButtonPress`: closes paywall when close button pressed. - `onAndroidSystemBack`: closes paywall when the **Back** button pressed. - `onRestoreCompleted`: closes paywall after successful restore. - `onPurchaseCompleted`: closes paywall unless user cancelled. - `onRenderingFailed`: closes paywall if its rendering fails. - `onUrlPress`: opens URLs in system browser and keeps paywall open. ### Event handlers | Event handler | Description | |:----------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **onCustomAction** | Invoked when a user performs a custom action, e.g., clicks a [custom button](paywall-buttons). | | **onUrlPress** | Invoked when a user clicks a URL in your paywall. | | **onAndroidSystemBack** | Invoked when a user taps the system Android **Back** button. | | **onCloseButtonPress** | Invoked when the close button is visible and a user taps it. It is recommended to dismiss the paywall screen in this handler. | | **onPurchaseCompleted** | Invoked when the purchase completes, whether successful, cancelled by user, or pending approval. In case of a successful purchase, it provides an updated `AdaptyProfile`. User cancellations and pending payments (e.g., parental approval required) trigger this event, not `onPurchaseFailed`. | | **onPurchaseStarted** | Invoked when a user taps the "Purchase" action button to start the purchase process. | | **onPurchaseCancelled** | Invoked when a user initiates the purchase process and manually interrupts it (cancels the payment dialog). | | **onPurchaseFailed** | Invoked when a purchase fails due to errors (e.g., payment restrictions, invalid products, network failures, transaction verification failures). Not invoked for user cancellations or pending payments, which trigger `onPurchaseCompleted` instead. | | **onRestoreStarted** | Invoked when a user starts a purchase restoration process. | | **onRestoreCompleted** | Invoked when purchase restoration succeeds and provides an updated `AdaptyProfile`. It is recommended to dismiss the screen if the user has the required `accessLevel`. Refer to the [Subscription status](capacitor-listen-subscription-changes) topic to learn how to check it. | | **onRestoreFailed** | Invoked when the restore process fails and provides `AdaptyError`. | | **onProductSelected** | Invoked when any product in the paywall view is selected, allowing you to monitor what the user selects before the purchase. | | **onAppeared** | Invoked when the paywall view appears on screen. On iOS, also invoked when a user taps the [web paywall button](web-paywall#step-2a-add-a-web-purchase-button) inside a paywall, and a web paywall opens in an in-app browser. | | **onDisappeared** | Invoked when the paywall view disappears from screen. On iOS, also invoked when a [web paywall](web-paywall#step-2a-add-a-web-purchase-button) opened from a paywall in an in-app browser disappears from the screen. | | **onRenderingFailed** | Invoked when an error occurs during view rendering and provides `AdaptyError`. Such errors should not occur, so if you come across one, please let us know. | | **onLoadingProductsFailed** | Invoked when product loading fails and provides `AdaptyError`. If you haven't set `prefetchProducts: true` in view creation, AdaptyUI will retrieve the necessary objects from the server by itself. | --- # File: capacitor-use-fallback-paywalls --- --- title: "Capacitor - Use fallback paywalls" description: "Handle cases when users are offline or Adapty servers aren't available" --- To maintain a fluid user experience, it is important to set up [fallbacks](/fallback-paywalls) for your [paywalls](paywalls) and [onboardings](onboardings). This precaution extends the application's capabilities in case of partial or complete loss of internet connection. * **If the application cannot access Adapty servers:** It will be able to display a fallback paywall, and access the local onboarding configuration. * **If the application cannot access the internet:** It will be able to display a fallback paywall. Onboardings include remote content and require an internet connection to function. :::important Before you follow the steps in this guide, [download](/local-fallback-paywalls) the fallback configuration files from Adapty. ::: ## Configuration ### Android 1. Add the fallback configuration file to your application. Select one of the following directories: * **android/app/src/main/assets/** * **android/app/src/main/res/raw/** Note: The `res/raw` folder has a special file naming convention (start with a letter, no capital letters, no special characters except for the underscore, and no spaces in the names). 2. Update the `android` property of the `FileLocation` constant: * If the file is located in the `assets` directory, pass the file's path relative to the directory. * If the file is located in the `res/raw` directory, pass the name of the file without the extension. ### iOS 1. Add the fallback JSON file to your project bundle: open the **File** menu in XCode and select the **Add Files to "YourProjectName"** option. 2. Pass the name of your configuration file to the `ios` property of the `FileLocation` constant. ## Example ```typescript showLineNumbers const fileLocation = { ios: { fileName: 'ios_fallback.json' }, android: { //if the file is located in 'android/app/src/main/assets/' relativeAssetPath: 'android_fallback.json' } }; await adapty.setFallback({ fileLocation }); ``` Parameters: | Parameter | Description | | :------------------- | :------------------------------------------------------- | | **fileLocation** | Object that represents the location of the fallback configuration file. | --- # File: capacitor-web-paywall --- --- title: "Implement web paywalls" description: "Learn how to implement web paywalls in your Capacitor app with Adapty SDK." --- :::important Before you begin, make sure you have [configured your web paywall in the dashboard](web-paywall.md) and installed Adapty SDK version 3.6.1 or later. ::: ## Open web paywalls If you are working with a paywall you developed yourself, you need to handle web paywalls using the SDK method. The `.openWebPaywall` method: 1. Generates a unique URL allowing Adapty to link a specific paywall shown to a particular user to the web page they are redirected to. 2. Tracks when your users return to the app and then requests `.getProfile` at short intervals to determine whether the profile access rights have been updated. This way, if the payment has been successful and access rights have been updated, the subscription activates in the app almost immediately. ```typescript showLineNumbers try { await adapty.openWebPaywall({ paywallOrProduct: product }); } catch (error) { console.error('Failed to open web paywall:', error); } ``` :::note There are two versions of the `openWebPaywall` method: 1. `openWebPaywall({ paywallOrProduct: product })` that generates URLs by paywall and adds the product data to URLs as well. 2. `openWebPaywall({ paywallOrProduct: paywall })` that generates URLs by paywall without adding the product data to URLs. Use it when your products in the Adapty paywall differ from those in the web paywall. ::: #### Handle errors | Error | Description | Recommended action | |-----------------------------------------|--------------------------------------------------------|---------------------------------------------------------------------------| | AdaptyError.paywallWithoutPurchaseUrl | The paywall doesn't have a web purchase URL configured | Check if the paywall has been properly configured in the Adapty Dashboard | | AdaptyError.productWithoutPurchaseUrl | The product doesn't have a web purchase URL | Verify the product configuration in the Adapty Dashboard | | AdaptyError.failedOpeningWebPaywallUrl | Failed to open the URL in the browser | Check device settings or provide an alternative purchase method | | AdaptyError.failedDecodingWebPaywallUrl | Failed to properly encode parameters in the URL | Verify URL parameters are valid and properly formatted | ## Open web paywalls in an in-app browser :::important Opening web paywalls in an in-app browser is supported starting from Adapty SDK v. 3.15. ::: By default, web paywalls open in the external browser. To provide a seamless user experience, you can open web paywalls in an in-app browser. This displays the web purchase page within your application, allowing users to complete transactions without switching apps. To enable this, set `openIn` to `WebPresentation.BrowserInApp` in `openWebPaywall`: ```typescript showLineNumbers try { await adapty.openWebPaywall({ paywallOrProduct: product, openIn: WebPresentation.BrowserInApp, // default – WebPresentation.BrowserOutApp }); } catch (error) { console.error('Failed to open web paywall:', error); } ``` --- # File: capacitor-implement-paywalls-manually --- --- title: "Implement paywalls manually" description: "Learn how to implement paywalls manually in your Capacitor app with Adapty SDK." --- ## Accept purchases If you are working with paywalls you've implemented yourself, you can delegate handling purchases to Adapty, using the `makePurchase` method. This way, we will handle all the user scenarios, and you will only need to handle the purchase results. :::important `makePurchase` works with products created in the Adapty dashboard. Make sure you configure products and ways to retrieve them in the dashboard by following the [quickstart guide](quickstart). ::: ## Observer mode If you want to implement your own purchase handling logic from scratch but still want to benefit from the advanced analytics in Adapty, you can use the observer mode. :::important Consider the observer mode limitations [here](observer-vs-full-mode). ::: --- # File: capacitor-quickstart-manual --- --- title: "Enable purchases in your custom paywall in Capacitor SDK" description: "Integrate Adapty SDK into your custom Capacitor paywalls to enable in-app purchases." --- This guide describes how to integrate Adapty into your custom paywalls. Keep full control over paywall implementation, while the Adapty SDK fetches products, handles new purchases, and restores previous ones. :::important **This guide is for developers who are implementing custom paywalls.** If you want the easiest way to enable purchases, use the [Adapty Paywall Builder](capacitor-quickstart-paywalls.md). With Paywall Builder, you create paywalls in a no-code visual editor, Adapty handles all purchase logic automatically, and you can test different designs without republishing your app. ::: ## Before you start ### Set up products To enable in-app purchases, you need to understand three key concepts: - [**Products**](product.md) – anything users can buy (subscriptions, consumables, lifetime access) - [**Paywalls**](paywalls.md) – configurations that define which products to offer. In Adapty, paywalls are the only way to retrieve products, but this design lets you modify products, prices, and offers without touching your app code. - [**Placements**](placements.md) – where and when you show paywalls in your app (like `main`, `onboarding`, `settings`). You set up paywalls for placements in the dashboard, then request them by placement ID in your code. This makes it easy to run A/B tests and show different paywalls to different users. Make sure you understand these concepts even if you work with your custom paywall. Basically, they are just your way to manage the products you sell in your app. To implement your custom paywall, you will need to create a **paywall** and add it to a **placement**. This setup allows you to retrieve your products. To understand what you need to do in the dashboard, follow the quickstart guide [here](quickstart.md). ### Manage users You can work either with or without backend authentication on your side. However, the Adapty SDK handles anonymous and identified users differently. Read the [identification quickstart guide](capacitor-quickstart-identify.md) to understand the specifics and ensure you are working with users properly. ## Step 1. Get products To retrieve products for your custom paywall, you need to: 1. Get the `paywall` object by passing [placement](placements.md) ID to the `getPaywall` method. 2. Get the products array for this paywall using the `getPaywallProducts` method. ```typescript showLineNumbers async function loadPaywall() { try { const paywall: AdaptyPaywall = await adapty.getPaywall({ placementId: 'YOUR_PLACEMENT_ID' }); const products: AdaptyPaywallProduct[] = await adapty.getPaywallProducts({ paywall }); // Use products to build your custom paywall UI } catch (error) { // Handle the error } } ``` ## Step 2. Accept purchases When a user taps on a product in your custom paywall, call the `makePurchase` method with the selected product. This will handle the purchase flow and return the updated profile. ```typescript showLineNumbers async function purchaseProduct(product: AdaptyPaywallProduct) { try { const result: AdaptyPurchaseResult = await adapty.makePurchase({ product }); if (result.type === 'success') { // Purchase successful, profile updated } else if (result.type === 'user_cancelled') { // User canceled the purchase } else if (result.type === 'pending') { // Purchase is pending (e.g., user will pay offline with cash) } } catch (error) { // Handle the error } } ``` ## Step 3. Restore purchases App stores require all apps with subscriptions to provide a way users can restore their purchases. Call the `restorePurchases` method when the user taps the restore button. This will sync their purchase history with Adapty and return the updated profile. ```typescript showLineNumbers async function restorePurchases() { try { const profile: AdaptyProfile = await adapty.restorePurchases(); // Restore successful, profile updated } catch (error) { // Handle the error } } ``` ## Next steps Your paywall is ready to be displayed in the app. Test your purchases in the [App Store sandbox](test-purchases-in-sandbox) or in [Google Play Store](testing-on-android) to make sure you can complete a test purchase from the paywall. To see how this works in a production-ready implementation, check out the [App.tsx](https://github.com/adaptyteam/AdaptySDK-Capacitor/blob/master/examples/adapty-devtools/src/screens/app/App.tsx) in our example app, which demonstrates purchase handling with proper error handling, loading states, and comprehensive SDK integration. Next, [check whether users have completed their purchase](capacitor-check-subscription-status.md) to determine whether to display the paywall or grant access to paid features. --- # File: fetch-paywalls-and-products-capacitor --- --- title: "Fetch paywalls and products for remote config paywalls in Capacitor SDK" description: "Fetch paywalls and products in Adapty Capacitor SDK to enhance user monetization." --- Before showcasing remote config and custom paywalls, you need to fetch the information about them. Please be aware that this topic refers to remote config and custom paywalls. For guidance on fetching paywalls for Paywall Builder-customized paywalls, please consult [Fetch Paywall Builder paywalls and their configuration](capacitor-get-pb-paywalls). :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. :::
Before you start fetching paywalls and products in your mobile app (click to expand) 1. [Create your products](create-product) in the Adapty Dashboard. 2. [Create a paywall and incorporate the products into your paywall](create-paywall) in the Adapty Dashboard. 3. [Create placements and incorporate your paywall into the placement](create-placement) in the Adapty Dashboard. 4. [Install Adapty SDK](sdk-installation-capacitor) in your mobile app.
## Fetch paywall information In Adapty, a [product](product) serves as a combination of products from both the App Store and Google Play. These cross-platform products are integrated into paywalls, enabling you to showcase them within specific mobile app placements. To display the products, you need to obtain a [Paywall](paywalls) from one of your [placements](placements) with `getPaywall` method. ```typescript showLineNumbers try { const paywall = await adapty.getPaywall({ placementId: 'YOUR_PLACEMENT_ID', locale: 'en', params: { fetchPolicy: 'reload_revalidating_cache_data', // Load from server, fallback to cache loadTimeoutMs: 5000 // 5 second timeout } }); // the requested paywall } catch (error) { console.error('Failed to fetch paywall:', error); } ``` | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the [Placement](placements). This is the value you specified when creating a placement in your Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

See [Localizations and locale codes](capacitor-localizations-and-locale-codes) for more information on locale codes and how we recommend using them.

| | **params.fetchPolicy** |

optional

default: `'reload_revalidating_cache_data'`

|

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `'return_cache_data_else_load'` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

| | **params.loadTimeoutMs** |

optional

default: 5000 ms

|

This value limits the timeout (in milliseconds) for this method. If the timeout is reached, cached data or local fallback will be returned.

Note that in rare cases this method can timeout slightly later than specified in `loadTimeoutMs`, since the operation may consist of different requests under the hood.

| **Don't hardcode product IDs.** The only ID you should hardcode is the placement ID. Paywalls are configured remotely, so the number of products and available offers can change at any time. Your app must handle these changes dynamically—if a paywall returns two products today and three tomorrow, display all of them without code changes. Response parameters: | Parameter | Description | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](https://capacitor.adapty.io/interfaces/adaptypaywall) object with: a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch products Once you have the paywall, you can query the product array that corresponds to it: ```typescript showLineNumbers try { const products = await adapty.getPaywallProducts({ paywall }); // the requested products list } catch (error) { console.error('Failed to fetch products:', error); } ``` Response parameters: | Parameter | Description | | :-------- |:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Products | List of [`AdaptyPaywallProduct`](https://capacitor.adapty.io/interfaces/adaptypaywallproduct) objects with: product identifier, product name, price, currency, subscription length, and several other properties. | When implementing your own paywall design, you will likely need access to these properties from the [`AdaptyPaywallProduct`](https://capacitor.adapty.io/interfaces/adaptypaywallproduct) object. Illustrated below are the most commonly used properties, but refer to the linked document for full details on all available properties. | Property | Description | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Title** | To display the title of the product, use `product.localizedTitle`. Note that the localization is based on the users' selected store country rather than the locale of the device itself. | | **Price** | To display a localized version of the price, use `product.price?.localizedString`. This localization is based on the locale info of the device. You can also access the price as a number using `product.price?.amount`. The value will be provided in the local currency. To get the associated currency symbol, use `product.price?.currencySymbol`. | | **Subscription Period** | To display the period (e.g. week, month, year, etc.), use `product.subscription?.localizedSubscriptionPeriod`. This localization is based on the locale of the device. To fetch the subscription period programmatically, use `product.subscription?.subscriptionPeriod`. From there you can access the `unit` property to get the length (i.e. 'day', 'week', 'month', 'year', or 'unknown'). The `numberOfUnits` value will get you the number of period units. For example, for a quarterly subscription, you'd see `'month'` in the unit property, and `3` in the numberOfUnits property. | | **Introductory Offer** | To display a badge or other indicator that a subscription contains an introductory offer, check out the `product.subscription?.offer?.phases` property. This is a list that can contain up to two discount phases: the free trial phase and the introductory price phase. Within each phase object are the following helpful properties:
• `paymentMode`: a string with values `'free_trial'`, `'pay_as_you_go'`, `'pay_up_front'`, and `'unknown'`. Free trials will be the `'free_trial'` type.
• `price`: The discounted price as a number. For free trials, look for `0` here.
• `localizedNumberOfPeriods`: a string localized using the device's locale describing the length of the offer. For example, a three day trial offer shows `'3 days'` in this field.
• `subscriptionPeriod`: Alternatively, you can get the individual details of the offer period with this property. It works in the same manner for offers as the previous section describes.
• `localizedSubscriptionPeriod`: A formatted subscription period of the discount for the user's locale. | ## Speed up paywall fetching with default audience paywall Typically, paywalls are fetched almost instantly, so you don't need to worry about speeding up this process. However, in cases where you have numerous audiences and paywalls, and your users have a weak internet connection, fetching a paywall may take longer than you'd like. In such situations, you might want to display a default paywall to ensure a smooth user experience rather than showing no paywall at all. To address this, you can use the `getPaywallForDefaultAudience` method, which fetches the paywall of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the paywall by the `getPaywall` method, as detailed in the [Fetch Paywall Information](fetch-paywalls-and-products-capacitor#fetch-paywall-information) section above. :::warning Why we recommend using `getPaywall` The `getPaywallForDefaultAudience` method comes with a few significant drawbacks: - **Potential backward compatibility issues**: If you need to show different paywalls for different app versions (current and future), you may face challenges. You'll either have to design paywalls that support the current (legacy) version or accept that users with the current (legacy) version might encounter issues with non-rendered paywalls. - **Loss of targeting**: All users will see the same paywall designed for the **All Users** audience, which means you lose personalized targeting (including based on countries, marketing attribution or your own custom attributes). If you're willing to accept these drawbacks to benefit from faster paywall fetching, use the `getPaywallForDefaultAudience` method as follows. Otherwise, stick to the `getPaywall` described [above](fetch-paywalls-and-products-capacitor#fetch-paywall-information). ::: ```typescript showLineNumbers try { const paywall = await adapty.getPaywallForDefaultAudience({ placementId: 'YOUR_PLACEMENT_ID', locale: 'en', params: { fetchPolicy: 'reload_revalidating_cache_data' // Load from server, fallback to cache } }); // the requested paywall } catch (error) { console.error('Failed to fetch default audience paywall:', error); } ``` :::note The `getPaywallForDefaultAudience` method is available starting from Capacitor SDK version 2.11.2. ::: | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the [Placement](placements). This is the value you specified when creating a placement in your Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

See [Localizations and locale codes](capacitor-localizations-and-locale-codes) for more information on locale codes and how we recommend using them.

| | **params.fetchPolicy** |

optional

default: `'reload_revalidating_cache_data'`

|

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `'return_cache_data_else_load'` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

| --- # File: present-remote-config-paywalls-capacitor --- --- title: "Render paywall designed by remote config in Capacitor SDK" description: "Discover how to present remote config paywalls in Adapty Capacitor SDK to personalize user experience." --- If you've customized a paywall using remote config, you'll need to implement rendering in your mobile app's code to display it to users. Since remote config offers flexibility tailored to your needs, you're in control of what's included and how your paywall view appears. We provide a method for fetching the remote configuration, giving you the autonomy to showcase your custom paywall configured via remote config. ## Get paywall remote config and present it To get a remote config of a paywall, access the `remoteConfig` property and extract the needed values. ```typescript showLineNumbers try { const paywall = await adapty.getPaywall({ placementId: 'YOUR_PLACEMENT_ID', params: { fetchPolicy: 'reload_revalidating_cache_data', // Load from server, fallback to cache loadTimeoutMs: 5000 // 5 second timeout } }); const headerText = paywall.remoteConfig?.['header_text']; } catch (error) { console.error('Failed to fetch paywall:', error); } ``` At this point, once you've received all the necessary values, it's time to render and assemble them into a visually appealing page. Ensure that the design accommodates various mobile phone screens and orientations, providing a seamless and user-friendly experience across different devices. :::warning Make sure to [record the paywall view event](present-remote-config-paywalls-capacitor#track-paywall-view-events) as described below, allowing Adapty analytics to capture information for funnels and A/B tests. ::: After you've done with displaying the paywall, continue with setting up a purchase flow. When the user makes a purchase, simply call `.makePurchase()` with the product from your paywall. For details on the`.makePurchase()` method, read [Making purchases](capacitor-making-purchases). We recommend [creating a backup paywall called a fallback paywall](capacitor-use-fallback-paywalls). This backup will display to the user when there's no internet connection or cache available, ensuring a smooth experience even in these situations. ## Track paywall view events Adapty assists you in measuring the performance of your paywalls. While we gather data on purchases automatically, logging paywall views needs your input because only you know when a customer sees a paywall. To log a paywall view event, simply call `.logShowPaywall(paywall)`, and it will be reflected in your paywall metrics in funnels and A/B tests. :::important Calling `.logShowPaywall(paywall)` is not needed if you are displaying paywalls created in the [paywall builder](adapty-paywall-builder.md). ::: ```typescript showLineNumbers try { await adapty.logShowPaywall({ paywall }); } catch (error) { console.error('Failed to log paywall view:', error); } ``` Request parameters: | Parameter | Presence | Description | | :---------- | :------- | :--------------------------------------------------------- | | **paywall** | required | An [`AdaptyPaywall`](https://capacitor.adapty.io/interfaces/adaptypaywall) object. | --- # File: capacitor-making-purchases --- --- title: "Make purchases in mobile app in Capacitor SDK" description: "Guide on handling in-app purchases and subscriptions using Adapty." --- Displaying paywalls within your mobile app is an essential step in offering users access to premium content or services. However, simply presenting these paywalls is enough to support purchases only if you use [Paywall Builder](adapty-paywall-builder) to customize your paywalls. If you don't use the Paywall Builder, you must use a separate method called `.makePurchase()` to complete a purchase and unlock the desired content. This method serves as the gateway for users to engage with the paywalls and proceed with their desired transactions. If your paywall has an active promotional offer for the product a user is trying to buy, Adapty will automatically apply it at the time of purchase. Make sure you've [done the initial configuration](quickstart) without skipping a single step. Without it, we can't validate purchases. ## Make purchase :::note **Using [Paywall Builder](adapty-paywall-builder)?** Purchases are processed automatically—you can skip this step. **Looking for step-by-step guidance?** Check out the [quickstart guide](capacitor-implement-paywalls-manually) for end-to-end implementation instructions with full context. ::: ```typescript showLineNumbers try { const result = await adapty.makePurchase({ product }); if (result.type === 'success') { const isSubscribed = result.profile?.accessLevels['YOUR_ACCESS_LEVEL']?.isActive; if (isSubscribed) { // Grant access to the paid features console.log('User is now subscribed!'); } } else if (result.type === 'user_cancelled') { console.log('Purchase cancelled by user'); } else if (result.type === 'pending') { console.log('Purchase is pending'); } } catch (error) { console.error('Purchase failed:', error); } ``` Request parameters: | Parameter | Presence | Description | | :---------- | :------- |:----------------------------------------------------------------------------------------------------------------------------| | **product** | required | An [`AdaptyPaywallProduct`](https://capacitor.adapty.io/interfaces/adaptypaywallproduct) object retrieved from the paywall. | Response parameters: | Parameter | Description | |---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **result** | An [`AdaptyPurchaseResult`](https://capacitor.adapty.io/types/adaptypurchaseresult) object with a `type` field indicating the purchase outcome (`'success'`, `'user_cancelled'`, or `'pending'`) and a `profile` field containing the updated [`AdaptyProfile`](https://capacitor.adapty.io/interfaces/adaptyprofile) on successful purchases. | ## Change subscription when making a purchase When a user opts for a new subscription instead of renewing the current one, the way it works depends on the app store: - For the App Store, the subscription is automatically updated within the subscription group. If a user purchases a subscription from one group while already having a subscription from another, both subscriptions will be active at the same time. - For Google Play, the subscription isn't automatically updated. You'll need to manage the switch in your mobile app code as described below. To replace the subscription with another one in Android, call `.makePurchase()` method with the additional parameter: ```typescript showLineNumbers try { const result = await adapty.makePurchase({ product, params: { android: { subscriptionUpdateParams: { oldSubVendorProductId: 'old_product_id', prorationMode: 'charge_prorated_price' }, isOfferPersonalized: true } } }); if (result.type === 'success') { const isSubscribed = result.profile?.accessLevels['YOUR_ACCESS_LEVEL']?.isActive; if (isSubscribed) { // Grant access to the paid features console.log('Subscription updated successfully!'); } } else if (result.type === 'user_cancelled') { console.log('Purchase cancelled by user'); } else if (result.type === 'pending') { console.log('Purchase is pending'); } } catch (error) { console.error('Purchase failed:', error); } ``` Additional request parameter: | Parameter | Presence | Description | | :--------- | :------- | :----------------------------------------------------------- | | **params** | optional | An object of the [`MakePurchaseParamsInput`](https://capacitor.adapty.io/types/makepurchaseparamsinput) type containing platform-specific purchase parameters. | The `MakePurchaseParamsInput` structure includes: ```typescript { android: { subscriptionUpdateParams: { oldSubVendorProductId: 'old_product_id', prorationMode: 'charge_prorated_price' }, isOfferPersonalized: true } } ``` You can read more about subscriptions and replacement modes in the Google Developer documentation: - [About replacement modes](https://developer.android.com/google/play/billing/subscriptions#replacement-modes) - [Recommendations from Google for replacement modes](https://developer.android.com/google/play/billing/subscriptions#replacement-recommendations) - Replacement mode [`CHARGE_PRORATED_PRICE`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#CHARGE_PRORATED_PRICE()). Note: this method is available only for subscription upgrades. Downgrades are not supported. - Replacement mode [`DEFERRED`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#DEFERRED()). Note: A real subscription change will occur only when the current subscription billing period ends. ### Manage prepaid plans (Android) If your app users can purchase [prepaid plans](https://developer.android.com/google/play/billing/subscriptions#prepaid-plans) (e.g., buy a non-renewable subscription for several months), you can enable [pending transactions](https://developer.android.com/google/play/billing/subscriptions#pending) for prepaid plans. ```typescript showLineNumbers await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { android: { enablePendingPrepaidPlans: true, }, } }); ``` ## Redeem Offer Code in iOS Since iOS 14.0, your users can redeem Offer Codes. Code redemption means using a special code, like a promotional or gift card code, to get free access to content or features in an app or on the App Store. To enable users to redeem offer codes, you can display the offer code redemption sheet by using the appropriate SDK method: ```typescript showLineNumbers try { await adapty.presentCodeRedemptionSheet(); } catch (error) { console.error('Failed to present code redemption sheet:', error); } ``` :::danger Based on our observations, the Offer Code Redemption sheet in some apps may not work reliably. We recommend redirecting the user directly to the App Store. In order to do this, you need to open the url of the following format: `https://apps.apple.com/redeem?ctx=offercodes&id={apple_app_id}&code={code}` ::: --- # File: capacitor-restore-purchase --- --- title: "Restore purchases in mobile app in Capacitor SDK" description: "Learn how to restore purchases in Adapty to ensure seamless user experience." --- Restoring Purchases in both iOS and Android is a feature that allows users to regain access to previously purchased content, such as subscriptions or in-app purchases, without being charged again. This feature is especially useful for users who may have uninstalled and reinstalled the app or switched to a new device and want to access their previously purchased content without paying again. :::note In paywalls built with [Paywall Builder](adapty-paywall-builder), purchases are restored automatically without additional code from you. If that's your case — you can skip this step. ::: To restore a purchase if you do not use the [Paywall Builder](adapty-paywall-builder) to customize the paywall, call `.restorePurchases()` method: ```typescript showLineNumbers try { const profile = await adapty.restorePurchases(); const isSubscribed = profile.accessLevels['YOUR_ACCESS_LEVEL']?.isActive; if (isSubscribed) { // Restore access to paid features console.log('Access restored successfully!'); } else { console.log('No active subscriptions found'); } } catch (error) { console.error('Failed to restore purchases:', error); } ``` Response parameters: | Parameter | Description | |---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **profile** | An [`AdaptyProfile`](https://capacitor.adapty.io/interfaces/adaptyprofile) object. This model contains info about access levels, subscriptions, and non-subscription purchases. Check the **access level status** to determine whether the user has access to the app. | --- # File: implement-observer-mode-capacitor --- --- title: "Implement Observer mode in Capacitor SDK" description: "Implement observer mode in Adapty to track user subscription events in Capacitor SDK." --- If you already have your own purchase infrastructure and aren't ready to fully switch to Adapty, you can explore [Observer mode](observer-vs-full-mode). In its basic form, Observer Mode offers advanced analytics and seamless integration with attribution and analytics systems. If this meets your needs, you only need to: 1. Turn it on when configuring the Adapty SDK by setting the `observerMode` parameter to `true`. Follow the setup instructions for [Capacitor](sdk-installation-capacitor#configure-adapty-sdk). 2. [Report transactions](report-transactions-observer-mode-capacitor) from your existing purchase infrastructure to Adapty. ### Observer mode setup Turn on the Observer mode if you handle purchases and subscription status yourself and use Adapty only for sending subscription events and analytics. :::important When running in the Observer mode, Adapty SDK won't close any transactions, so make sure you're handling it. ::: ```typescript showLineNumbers try { await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { observerMode: true // Enable observer mode } }); } catch (error) { console.error('Failed to activate Adapty:', error); } ``` Parameters: | Parameter | Description | | --------------------------- | ------------------------------------------------------------ | | **observerMode** | A boolean value that controls [Observer mode](observer-vs-full-mode). The default value is `false`. | ## Using Adapty paywalls in Observer Mode If you also want to use Adapty's paywalls and A/B testing features, you can — but it requires some extra setup in Observer mode. Here's what you'll need to do in addition to the steps above: 1. Display paywalls as usual for [remote config paywalls](present-remote-config-paywalls-capacitor.md). 2. [Associate paywalls](report-transactions-observer-mode-capacitor) with purchase transactions. --- # File: report-transactions-observer-mode-capacitor --- --- title: "Report transactions in Observer Mode in Capacitor SDK" description: "Report purchase transactions in Adapty Observer Mode for user insights and revenue tracking in Capacitor SDK." --- In Observer mode, the Adapty SDK can't track purchases made through your existing purchase system on its own. You need to report transactions from your app store. It's crucial to set this up **before** releasing your app to avoid errors in analytics. Use `reportTransaction` to explicitly report each transaction for Adapty to recognize it. :::warning **Don't skip transaction reporting!** If you don't call `reportTransaction`, Adapty won't recognize the transaction, it won't appear in analytics, and it won't be sent to integrations. ::: If you use Adapty paywalls, include the `variationId` when reporting a transaction. This links the purchase to the paywall that triggered it, ensuring accurate paywall analytics. ```typescript showLineNumbers const variationId = paywall.variationId; try { await adapty.reportTransaction({ transactionId: 'your_transaction_id', variationId: variationId }); } catch (error) { console.error('Failed to report transaction:', error); } ``` Parameters: | Parameter | Presence | Description | | ------------- | -------- | ------------------------------------------------------------ | | **transactionId** | required |
  • For iOS: Identifier of the transaction.
  • For Android: String identifier (`purchase.getOrderId`) of the purchase, where the purchase is an instance of the billing library [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) class.
| | **variationId** | optional | The string identifier of the variation. You can get it using `variationId` property of the [AdaptyPaywall](https://capacitor.adapty.io/interfaces/adaptypaywall) object. | --- # File: capacitor-user --- --- title: "Users & access" description: "Learn how to work with users and access levels in your Capacitor app with Adapty SDK." --- --- # File: capacitor-identifying-users --- --- title: "Identify users in Capacitor SDK" description: "Learn how to identify users in your Capacitor app with Adapty SDK." --- Adapty creates an internal profile ID for every user. However, if you have your own authentication system, you should set your own Customer User ID. You can find users by their Customer User ID in the [Profiles](profiles-crm) section and use it in the [server-side API](getting-started-with-server-side-api), which will be sent to all integrations. ### Setting customer user ID on configuration If you have a user ID during configuration, just pass it as `customerUserId` parameter to `.activate()` method: ```typescript showLineNumbers try { await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { customerUserId: 'YOUR_USER_ID' } }); } catch (error) { console.error('Failed to activate Adapty:', error); } ``` ### Setting customer user ID after configuration If you don't have a user ID in the SDK configuration, you can set it later at any time with the `.identify()` method. The most common cases for using this method are after registration or authorization, when the user switches from being an anonymous user to an authenticated user. ```typescript showLineNumbers try { await adapty.identify({ customerUserId: 'YOUR_USER_ID' }); console.log('User identified successfully'); } catch (error) { console.error('Failed to identify user:', error); } ``` Request parameters: | Parameter | Presence | Description | |---------|--------|-----------| | **customerUserId** | required | A string user identifier. | :::warning Resubmitting of significant user data In some cases, such as when a user logs into their account again, Adapty's servers already have information about that user. In these scenarios, the Adapty SDK will automatically switch to work with the new user. If you passed any data to the anonymous user, such as custom attributes or attributions from third-party networks, you should resubmit that data for the identified user. It's also important to note that you should re-request all paywalls and products after identifying the user, as the new user's data may be different. ::: ### Logging out and logging in You can logout the user anytime by calling `.logout()` method: ```typescript showLineNumbers try { await adapty.logout(); console.log('User logged out successfully'); } catch (error) { console.error('Failed to logout user:', error); } ``` You can then login the user using `.identify()` method. ## Assign `appAccountToken` (iOS) [`appAccountToken`](https://developer.apple.com/documentation/storekit/product/purchaseoption/appaccounttoken(_:)) is a **UUID** that lets you link App Store transactions to your internal user identity. StoreKit associates this token with every transaction, so your backend can match App Store data to your users. Use a stable UUID generated per user and reuse it for the same account across devices. This ensures that purchases and App Store notifications stay correctly linked. You can set the token in two ways – during the SDK activation or when identifying the user. :::important You must always pass `appAccountToken` together with `customerUserId`. If you pass only the token, it will not be included in the transaction. ::: ```typescript showLineNumbers // During configuration: await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { customerUserId: 'YOUR_USER_ID', ios: { appAccountToken: "YOUR_APP_ACCOUNT_TOKEN" }, } }); // Or when identifying users await adapty.identify({ customerUserId: 'YOUR_USER_ID', params: { ios: { appAccountToken: 'YOUR_APP_ACCOUNT_TOKEN' }, } }); ``` ### Set obfuscated account IDs (Android) Google Play requires obfuscated account IDs for certain use cases to enhance user privacy and security. These IDs help Google Play identify purchases while keeping user information anonymous, which is particularly important for fraud prevention and analytics. You may need to set these IDs if your app handles sensitive user data or if you're required to comply with specific privacy regulations. The obfuscated IDs allow Google Play to track purchases without exposing actual user identifiers. ```typescript showLineNumbers // During configuration: await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { android: { obfuscatedAccountId: 'YOUR_OBFUSCATED_ACCOUNT_ID' }, } }); // Or when identifying users await adapty.identify({ customerUserId: 'YOUR_USER_ID', params: { android: { obfuscatedAccountId: 'YOUR_OBFUSCATED_ACCOUNT_ID' }, } }); ``` --- # File: capacitor-setting-user-attributes --- --- title: "Set user attributes in Capacitor SDK" description: "Learn how to update user attributes and profile data in your Capacitor app with Adapty SDK." --- You can set optional attributes such as email, phone number, etc, to the user of your app. You can then use attributes to create user [segments](segments) or just view them in CRM. ### Setting user attributes To set user attributes, call `.updateProfile()` method: ```typescript showLineNumbers const params = { email: 'email@email.com', phoneNumber: '+18888888888', firstName: 'John', lastName: 'Appleseed', gender: 'other', birthday: new Date().toISOString(), }; try { await adapty.updateProfile(params); console.log('Profile updated successfully'); } catch (error) { console.error('Failed to update profile:', error); } ``` Please note that the attributes that you've previously set with the `updateProfile` method won't be reset. :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ### The allowed keys list The allowed keys of `AdaptyProfileParameters` and their values are listed below: | Key | Value | |---|-----| | **email** | String | | **phoneNumber** | String | | **firstName** | String | | **lastName** | String | | **gender** | Enum, allowed values are: `'female'`, `'male'`, `'other'` | | **birthday** | Date string in ISO format | ### Custom user attributes You can set your own custom attributes. These are usually related to your app usage. For example, for fitness applications, they might be the number of exercises per week, for language learning app user's knowledge level, and so on. You can use them in segments to create targeted paywalls and offers, and you can also use them in analytics to figure out which product metrics affect the revenue most. ```typescript showLineNumbers try { await adapty.updateProfile({ codableCustomAttributes: { key_1: 'value_1', key_2: 2, }, }); console.log('Custom attributes updated successfully'); } catch (error) { console.error('Failed to update custom attributes:', error); } ``` To remove existing keys, pass `null` as their values: ```typescript showLineNumbers try { // to remove keys, pass null as their values await adapty.updateProfile({ codableCustomAttributes: { key_1: null, key_2: null, }, }); console.log('Custom attributes removed successfully'); } catch (error) { console.error('Failed to remove custom attributes:', error); } ``` Sometimes you need to figure out what custom attributes have already been installed before. To do this, use the `customAttributes` field of the `AdaptyProfile` object. :::warning Keep in mind that the value of `customAttributes` may be out of date since the user attributes can be sent from different devices at any time so the attributes on the server might have been changed after the last sync. ::: ### Limits - Up to 30 custom attributes per user - Key names are up to 30 characters long. The key name can include alphanumeric characters and any of the following: `_` `-` `.` - Value can be a string or float with no more than 50 characters. --- # File: capacitor-listen-subscription-changes --- --- title: "Check subscription status in Capacitor SDK" description: "Track and manage user subscription status in Adapty for improved customer retention in your Capacitor app." --- With Adapty, keeping track of subscription status is made easy. You don't have to manually insert product IDs into your code. Instead, you can effortlessly confirm a user's subscription status by checking for an active [access level](access-level).
Before you start checking subscription status (Click to Expand) - For iOS, set up [App Store Server Notifications](enable-app-store-server-notifications) - For Android, set up [Real-time Developer Notifications (RTDN)](enable-real-time-developer-notifications-rtdn)
## Access level and the AdaptyProfile object Access levels are properties of the [AdaptyProfile](https://capacitor.adapty.io/interfaces/adaptyprofile) object. We recommend retrieving the profile when your app starts, such as when you [identify a user](capacitor-identifying-users#setting-customer-user-id-on-configuration) , and then updating it whenever changes occur. This way, you can use the profile object without repeatedly requesting it. To be notified of profile updates, listen for profile changes as described in the [Listening for profile updates, including access levels](capacitor-listen-subscription-changes.md) section below. :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ## Retrieving the access level from the server To get the access level from the server, use the `.getProfile()` method: ```typescript showLineNumbers try { const profile = await adapty.getProfile(); console.log('Profile retrieved successfully'); } catch (error) { console.error('Failed to get profile:', error); } ``` Response parameters: | Parameter | Description | | --------- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **profile** | An [AdaptyProfile](https://capacitor.adapty.io/interfaces/adaptyprofile) object. Generally, you have to check only the access level status of the profile to determine whether the user has premium access to the app. The `.getProfile` method provides the most up-to-date result as it always tries to query the API. If for some reason (e.g. no internet connection), the Adapty SDK fails to retrieve information from the server, the data from the cache will be returned. It is also important to note that the Adapty SDK updates `AdaptyProfile` cache regularly, to keep this information as up-to-date as possible. | The `.getProfile()` method provides you with the user profile from which you can get the access level status. You can have multiple access levels per app. For example, if you have a newspaper app and sell subscriptions to different topics independently, you can create access levels "sports" and "science". But most of the time, you will only need one access level, in that case, you can just use the default "premium" access level. Here is an example for checking for the default "premium" access level: ```typescript showLineNumbers try { const profile = await adapty.getProfile(); const isActive = profile.accessLevels['premium']?.isActive; if (isActive) { // Grant access to premium features console.log('User has premium access'); } else { console.log('User does not have premium access'); } } catch (error) { console.error('Failed to check subscription status:', error); } ``` ### Listening for subscription status updates Whenever the user's subscription changes, Adapty fires an event. To receive messages from Adapty, you need to make some additional configuration: ```typescript showLineNumbers // Create an "onLatestProfileLoad" event listener adapty.addListener('onLatestProfileLoad', (data) => { const profile = data.profile; const isActive = profile.accessLevels['premium']?.isActive; if (isActive) { console.log('Subscription status updated: User has premium access'); } else { console.log('Subscription status updated: User does not have premium access'); } }); ``` Adapty also fires an event at the start of the application. In this case, the cached subscription status will be passed. ### Subscription status cache The cache implemented in the Adapty SDK stores the subscription status of the profile. This means that even if the server is unavailable, the cached data can be accessed to provide information about the profile's subscription status. However, it's important to note that direct data requests from the cache are not possible. The SDK periodically queries the server every minute to check for any updates or changes related to the profile. If there are any modifications, such as new transactions or other updates, they will be sent to the cached data in order to keep it synchronized with the server. --- # File: capacitor-deal-with-att --- --- title: "Deal with ATT in Capacitor SDK" description: "Get started with Adapty on Capacitor to streamline subscription setup and management." --- If your application uses AppTrackingTransparency framework and presents an app-tracking authorization request to the user, then you should send the [authorization status](https://developer.apple.com/documentation/apptrackingtransparency/attrackingmanager/authorizationstatus/) to Adapty. ```typescript showLineNumbers try { await adapty.updateProfile({ appTrackingTransparencyStatus: AppTrackingTransparencyStatus.Authorized, }); console.log('ATT status updated successfully'); } catch (error) { console.error('Failed to update ATT status:', error); } ``` :::warning We strongly recommend that you send this value as early as possible when it changes, only in that case the data will be sent in a timely manner to the integrations you have configured. ::: --- # File: capacitor-onboardings --- --- title: "Onboardings" description: "Learn how to work with onboardings in your Capacitor app with Adapty SDK." --- --- # File: capacitor-get-onboardings --- --- title: "Get onboardings in Capacitor SDK" description: "Learn how to retrieve onboardings in Adapty for Capacitor." --- After [you designed the visual part for your onboarding](design-onboarding.md) with the builder in the Adapty Dashboard, you can display it in your Capacitor app. The first step in this process is to get the onboarding associated with the placement and its view configuration as described below. Before you start, ensure that: 1. You have [created an onboarding](create-onboarding.md). 2. You have added the onboarding to a [placement](placements.md). ## Fetch onboarding When you create an [onboarding](onboardings.md) with our no-code builder, it's stored as a container with configuration that your app needs to fetch and display. This container manages the entire experience - what content appears, how it's presented, and how user interactions (like quiz answers or form inputs) are processed. The container also automatically tracks analytics events, so you don't need to implement separate view tracking. For best performance, fetch the onboarding configuration early to give images enough time to download before showing to users. To get an onboarding, use the `getOnboarding` method: ```typescript showLineNumbers try { const onboarding = await adapty.getOnboarding({ placementId: 'YOUR_PLACEMENT_ID', locale: 'en', params: { fetchPolicy: 'reload_revalidating_cache_data', // Load from server, fallback to cache loadTimeoutMs: 5000 // 5 second timeout } }); console.log('Onboarding fetched successfully'); } catch (error) { console.error('Failed to fetch onboarding:', error); } ``` Then, call the `createOnboardingView` method to create a view instance. :::warning The result of the `createOnboardingView` method can only be used once. If you need to use it again, call the `createOnboardingView` method anew. ::: ```typescript showLineNumbers if (onboarding.hasViewConfiguration) { try { const view = await createOnboardingView(onboarding); console.log('Onboarding view created successfully'); } catch (error) { console.error('Failed to create onboarding view:', error); } } else { // Use your custom logic console.log('Onboarding does not have view configuration'); } ``` Parameters: | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the onboarding localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.

| | **params.fetchPolicy** |

optional

default: `'reload_revalidating_cache_data'`

|

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `'return_cache_data_else_load'` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

| | **params.loadTimeoutMs** |

optional

default: 5000 ms

|

This value limits the timeout (in milliseconds) for this method. If the timeout is reached, cached data or local fallback will be returned.

Note that in rare cases this method can timeout slightly later than specified in `loadTimeoutMs`, since the operation may consist of different requests under the hood.

| Response parameters: | Parameter | Description | |:----------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **onboarding** | An [`AdaptyOnboarding`](https://capacitor.adapty.io/interfaces/adaptyonboarding) object with: the onboarding identifier and configuration, remote config, and several other properties. | ## Speed up onboarding fetching with default audience onboarding Typically, onboardings are fetched almost instantly, so you don't need to worry about speeding up this process. However, in cases where you have numerous audiences and onboardings, and your users have a weak internet connection, fetching a onboarding may take longer than you'd like. In such situations, you might want to display a default onboarding to ensure a smooth user experience rather than showing no onboarding at all. To address this, you can use the `getOnboardingForDefaultAudience` method, which fetches the onboarding of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the onboarding by the `getOnboarding` method, as detailed in the [Fetch Onboarding](#fetch-onboarding) section above. :::warning Consider using `getOnboarding` instead of `getOnboardingForDefaultAudience`, as the latter has important limitations: - **Compatibility issues**: May create problems when supporting multiple app versions, requiring either backward-compatible designs or accepting that older versions might display incorrectly. - **No personalization**: Only shows content for the "All Users" audience, removing targeting based on country, attribution, or custom attributes. If faster fetching outweighs these drawbacks for your use case, use `getOnboardingForDefaultAudience` as shown below. Otherwise, use `getOnboarding` as described [above](#fetch-onboarding). ::: ```typescript showLineNumbers try { const onboarding = await adapty.getOnboardingForDefaultAudience({ placementId: 'YOUR_PLACEMENT_ID', locale: 'en', params: { fetchPolicy: 'reload_revalidating_cache_data' // Load from server, fallback to cache } }); console.log('Default audience onboarding fetched successfully'); } catch (error) { console.error('Failed to fetch default audience onboarding:', error); } ``` Parameters: | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the onboarding localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.

| | **params.fetchPolicy** |

optional

default: `'reload_revalidating_cache_data'`

|

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `'return_cache_data_else_load'` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

| --- # File: capacitor-present-onboardings --- --- title: "Present onboardings in Capacitor SDK" description: "Discover how to present onboardings on Capacitor to boost conversions and revenue." --- If you've customized an onboarding using the builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such an onboarding contains both what should be shown within the onboarding and how it should be shown. Before you start, ensure that: 1. You have [created an onboarding](create-onboarding.md). 2. You have added the onboarding to a [placement](placements.md). ## Present onboarding To display an onboarding, use the `view.present()` method on the `view` created by the `createOnboardingView` method. Each `view` can only be used once. If you need to display the onboarding again, call `createOnboardingView` one more time to create a new `view` instance. :::warning Reusing the same `view` without recreating it may result in an error. ::: ```typescript showLineNumbers try { const view = await createOnboardingView(onboarding); view.setEventHandlers({ onClose: (actionId, meta) => { console.log('Onboarding closed:', actionId); return true; // Allow the onboarding to close }, onCustom: (actionId, meta) => { console.log('Custom action:', actionId); return false; // Don't close the onboarding } }); await view.present(); console.log('Onboarding presented successfully'); } catch (error) { console.error('Failed to present onboarding:', error); } ``` ## Configure iOS presentation style Configure how the onboarding is presented on iOS by passing the `iosPresentationStyle` parameter to the `present()` method. The parameter accepts `'full_screen'` (default) or `'page_sheet'` values. ```typescript showLineNumbers await view.present({ iosPresentationStyle: 'page_sheet' }); ``` ## Customize how links open in onboardings :::important Customizing how links open in onboardings is supported starting from Adapty SDK v.3.15. ::: By default, links in onboardings open in an in-app browser. This provides a seamless user experience by displaying web pages within your application, allowing users to view them without switching apps. If you prefer to open links in an external browser instead, you can customize this behavior by setting the `openIn` parameter to `browser_out_app`: ```typescript showLineNumbers await view.present({ openIn: 'browser_out_app' }); // default — browser_in_app ``` ## Next steps Once you've presented your onboarding, you'll want to [handle user interactions and events](capacitor-handling-onboarding-events.md). Learn how to handle onboarding events to respond to user actions and track analytics. --- # File: capacitor-handling-onboarding-events --- --- title: "Handle onboarding events in Capacitor SDK" description: "Handle onboarding-related events in Capacitor using Adapty." --- Onboardings configured with the builder generate events your app can respond to. Use the `setEventHandlers` method to handle these events for standalone screen presentation. Before you start, ensure that: 1. You have [created an onboarding](create-onboarding.md). 2. You have added the onboarding to a [placement](placements.md). ## Set up event handlers To handle events for onboardings, use the `view.setEventHandlers` method: ```typescript showLineNumbers try { const view = await createOnboardingView(onboarding); view.setEventHandlers({ onAnalytics(event, meta) { console.log('Analytics event:', event); }, onClose(actionId, meta) { console.log('Onboarding closed:', actionId); return true; // Allow the onboarding to close }, onCustom(actionId, meta) { console.log('Custom action:', actionId); return false; // Don't close the onboarding }, onPaywall(actionId, meta) { console.log('Paywall action:', actionId); view.dismiss().then(() => { openPaywall(actionId); }); }, onStateUpdated(action, meta) { console.log('State updated:', action); }, onFinishedLoading(meta) { console.log('Onboarding finished loading'); }, onError(error) { console.error('Onboarding error:', error); }, }); await view.present(); } catch (error) { console.error('Failed to present onboarding:', error); } ``` ## Event types The following sections describe the different types of events you can handle. ### Handle custom actions In the builder, you can add a **custom** action to a button and assign it an ID. Then, you can use this ID in your code and handle it as a custom action. For example, if a user taps a custom button, like **Login** or **Allow notifications**, the event handler will be triggered with the `actionId` parameter that matches the **Action ID** from the builder. You can create your own IDs, like "allowNotifications". ```typescript showLineNumbers view.setEventHandlers({ onCustom(actionId, meta) { switch (actionId) { case 'login': console.log('Login action triggered'); break; case 'allow_notifications': console.log('Allow notifications action triggered'); break; } return false; // Don't close the onboarding }, }); ```
Event example (Click to expand) ```json { "actionId": "allow_notifications", "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "screensTotal": 3 } } ```
### Finishing loading onboarding When an onboarding finishes loading, this event will be triggered: ```typescript showLineNumbers view.setEventHandlers({ onFinishedLoading(meta) { console.log('Onboarding loaded:', meta.onboardingId); }, }); ```
Event example (Click to expand) ```json { "meta": { "onboarding_id": "onboarding_123", "screen_cid": "welcome_screen", "screen_index": 0, "total_screens": 4 } } ```
### Closing onboarding Onboarding is considered closed when a user taps a button with the **Close** action assigned. :::important Note that you need to manage what happens when a user closes the onboarding. For instance, you need to stop displaying the onboarding itself. ::: ```typescript showLineNumbers view.setEventHandlers({ onClose(actionId, meta) { console.log('Onboarding closed:', actionId); return true; // Allow the onboarding to close }, }); ```
Event example (Click to expand) ```json { "action_id": "close_button", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "final_screen", "screen_index": 3, "total_screens": 4 } } ```
### Opening a paywall :::tip Handle this event to open a paywall if you want to open it inside the onboarding. If you want to open a paywall after it is closed, there is a more straightforward way to do it – handle the close action and open a paywall without relying on the event data. ::: If a user clicks a button that opens a paywall, you will get a button action ID that you [set up manually](get-paid-in-onboardings.md). The most seamless way to work with paywalls in onboardings is to make the action ID equal to a paywall placement ID. Note that, for iOS, only one view (paywall or onboarding) can be displayed on screen at a time. If you present a paywall on top of an onboarding, you cannot programmatically control the onboarding in the background. Attempting to dismiss the onboarding will close the paywall instead, leaving the onboarding visible. To avoid this, always dismiss the onboarding view before presenting the paywall. ```typescript showLineNumbers view.setEventHandlers({ onPaywall(actionId, meta) { // Dismiss onboarding before presenting paywall view.dismiss().then(() => { openPaywall(actionId); }); }, }); async function openPaywall(placementId: string) { // Implement your paywall opening logic here } ```
Event example (Click to expand) ```json { "action_id": "premium_offer_1", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "pricing_screen", "screen_index": 2, "total_screens": 4 } } ```
### Tracking navigation You receive an analytics event when various navigation-related events occur during the onboarding flow: ```typescript showLineNumbers view.setEventHandlers({ onAnalytics(event, meta) { console.log('Analytics event:', event.type, meta.onboardingId); }, }); ``` The `event` object can be one of the following types: |Type | Description | |------------|-------------| | `onboardingStarted` | When the onboarding has been loaded | | `screenPresented` | When any screen is shown | | `screenCompleted` | When a screen is completed. Includes optional `elementId` (identifier of the completed element) and optional `reply` (response from the user). Triggered when users perform any action to exit the screen. | | `secondScreenPresented` | When the second screen is shown | | `userEmailCollected` | Triggered when the user's email is collected via the input field | | `onboardingCompleted` | Triggered when a user reaches a screen with the `final` ID. If you need this event, [assign the `final` ID to the last screen](design-onboarding.md). | | `unknown` | For any unrecognized event type. Includes `name` (the name of the unknown event) and `meta` (additional metadata) | Each event includes `meta` information containing: | Field | Description | |------------|-------------| | `onboardingId` | Unique identifier of the onboarding flow | | `screenClientId` | Identifier of the current screen | | `screenIndex` | Current screen's position in the flow | | `screensTotal` | Total number of screens in the flow |
Event examples (Click to expand) ```javascript // onboardingStarted { "name": "onboarding_started", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "welcome_screen", "screen_index": 0, "total_screens": 4 } } // screenPresented { "name": "screen_presented", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "interests_screen", "screen_index": 2, "total_screens": 4 } } // screenCompleted { "name": "screen_completed", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 1, "total_screens": 4 }, "params": { "element_id": "profile_form", "reply": "success" } } // secondScreenPresented { "name": "second_screen_presented", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 1, "total_screens": 4 } } // userEmailCollected { "name": "user_email_collected", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 1, "total_screens": 4 } } // onboardingCompleted { "name": "onboarding_completed", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "final_screen", "screen_index": 3, "total_screens": 4 } } ```
--- # File: capacitor-onboarding-input --- --- title: "Process data from onboardings in Capacitor SDK" description: "Save and use data from onboardings in your Capacitor app with Adapty SDK." --- When your users respond to a quiz question or input their data into an input field, the `onStateUpdatedAction` method will be invoked. You can save or process the field type in your code. For example: ```typescript view.setEventHandlers({ onStateUpdated(action, meta) { // Process data }, }); ``` See the action format [here](https://capacitor.adapty.io/types/onboardingstateupdatedaction).
Saved data examples (the format may differ in your implementation) ```javascript // Example of a saved select action { "elementId": "preference_selector", "meta": { "onboardingId": "onboarding_123", "screenClientId": "preferences_screen", "screenIndex": 1, "screensTotal": 3 }, "params": { "type": "select", "value": { "id": "option_1", "value": "premium", "label": "Premium Plan" } } } // Example of a saved multi-select action { "elementId": "interests_selector", "meta": { "onboardingId": "onboarding_123", "screenClientId": "interests_screen", "screenIndex": 2, "screensTotal": 3 }, "params": { "type": "multiSelect", "value": [ { "id": "interest_1", "value": "sports", "label": "Sports" }, { "id": "interest_2", "value": "music", "label": "Music" } ] } } // Example of a saved input action { "elementId": "name_input", "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "screensTotal": 3 }, "params": { "type": "input", "value": { "type": "text", "value": "John Doe" } } } // Example of a saved date picker action { "elementId": "birthday_picker", "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "screensTotal": 3 }, "params": { "type": "datePicker", "value": { "day": 15, "month": 6, "year": 1990 } } } ```
## Use cases ### Enrich user profiles with data If you want to immediately link the input data with the user profile and avoid asking them twice for the same info, you need to [update the user profile](capacitor-setting-user-attributes.md) with the input data when handling the action. For example, you ask users to enter their name in the text field with the `name` ID, and you want to set this field's value as user's first name. Also, you ask them to enter their email in the `email` field. In your app code, it can look like this: ```typescript showLineNumbers view.setEventHandlers({ onStateUpdated(action, meta) { // Store user preferences or responses if (action.elementType === 'input') { const profileParams: any = {}; // Map elementId to appropriate profile field switch (action.elementId) { case 'name': if (action.value.type === 'text') { profileParams.firstName = action.value.value; } break; case 'email': if (action.value.type === 'email') { profileParams.email = action.value.value; } break; } // Update profile if we have data to update if (Object.keys(profileParams).length > 0) { adapty.updateProfile({ params: profileParams }).catch((error) => { // handle the error }); } } }, }); ``` ### Customize paywalls based on answers Using quizzes in onboardings, you can also customize paywalls you show users after they complete the onboarding. For example, you can ask users about their experience with sport and show different CTAs and products to different user groups. 1. [Add a quiz](onboarding-quizzes.md) in the onboarding builder and assign meaningful IDs to its options. 2. Handle the quiz responses based on their IDs and [set custom attributes](capacitor-setting-user-attributes.md) for users. ```typescript showLineNumbers view.setEventHandlers({ onStateUpdated(action, meta) { // Handle quiz responses and set custom attributes if (action.elementType === 'select') { const profileParams: any = {}; // Map quiz responses to custom attributes switch (action.elementId) { case 'experience': // Set custom attribute 'experience' with the selected value (beginner, amateur, pro) profileParams.codableCustomAttributes = { experience: action.value.value }; break; } // Update profile if we have data to update if (Object.keys(profileParams).length > 0) { adapty.updateProfile({ params: profileParams }).catch((error) => { // handle the error }); } } }, }); ``` 3. [Create segments](segments.md) for each custom attribute value. 4. Create a [placement](placements.md) and add [audiences](audience.md) for each segment you've created. 5. [Display a paywall](capacitor-paywalls.md) for the placement in your app code. If your onboarding has a button that opens a paywall, implement the paywall code as a [response to this button's action](capacitor-handling-onboarding-events#opening-a-paywall). --- # File: capacitor-test --- --- title: "Test & release in Capacitor SDK" description: "Learn how to test and release your Capacitor app with Adapty SDK." --- If you've already implemented the Adapty SDK in your Capacitor app, you'll want to test that everything is set up correctly and that purchases work as expected across both iOS and Android platforms. This involves testing both the SDK integration and the actual purchase flow with Apple's sandbox environment and Google Play's testing environment. ## Test your app For comprehensive testing of your in-app purchases, see our platform-specific testing guides: [iOS testing guide](test-purchases-in-sandbox.md) and [Android testing guide](testing-on-android.md). ## Prepare for release Before submitting your app to the store, follow the [Release checklist](release-checklist) to confirm: - Store connection and server notifications are configured - Purchases complete and are reported to Adapty - Access unlocks and restores correctly - Privacy and review requirements are met --- # File: kids-mode-capacitor --- --- title: "Kids Mode in Capacitor SDK" description: "Easily enable Kids Mode to comply with Apple and Google policies. No IDFA, GAID, or ad data collected in Capacitor SDK." --- If your Capacitor application is intended for kids, you must follow the policies of [Apple](https://developer.apple.com/kids/) and [Google](https://support.google.com/googleplay/android-developer/answer/9893335). If you're using the Adapty SDK, a few simple steps will help you configure it to meet these policies and pass app store reviews. ## What's required? You need to configure the Adapty SDK to disable the collection of: - [IDFA (Identifier for Advertisers)](https://en.wikipedia.org/wiki/Identifier_for_Advertisers) (iOS) - [Android Advertising ID (AAID/GAID)](https://support.google.com/googleplay/android-developer/answer/6048248) (Android) - [IP address](https://www.ftc.gov/system/files/ftc_gov/pdf/p235402_coppa_application.pdf) In addition, we recommend using customer user ID carefully. User ID in format `` will be definitely treated as gathering personal data as well as using email. For Kids Mode, a best practice is to use randomized or anonymized identifiers (e.g., hashed IDs or device-generated UUIDs) to ensure compliance. ## Enabling Kids Mode ### Updates in the Adapty Dashboard In the Adapty Dashboard, you need to disable the IP address collection. To do this, go to [App settings](https://app.adapty.io/settings/general) and click **Disable IP address collection** under **Collect users' IP address**. ### Updates in your mobile app code In order to comply with policies, disable the collection of the user's IDFA, GAID, and IP address: ```typescript showLineNumbers try { await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { // Disable IP address collection ipAddressCollectionDisabled: true, // Disable IDFA collection on iOS ios: { idfaCollectionDisabled: true }, // Disable Google Advertising ID collection on Android android: { adIdCollectionDisabled: true } } }); console.log('Adapty activated with Kids Mode enabled'); } catch (error) { console.error('Failed to activate Adapty with Kids Mode:', error); } ``` ### Platform-specific configurations #### iOS: Enable Kids Mode using CocoaPods If you're using CocoaPods for iOS, you can also enable Kids Mode at the native level: 1. Update your Podfile: - If you **don't** have a `post_install` section, add the entire code block below. - If you **do** have a `post_install` section, merge the highlighted lines into it. ```ruby showLineNumbers title="Podfile" post_install do |installer| installer.pods_project.targets.each do |target| # highlight-start if target.name == 'Adapty' target.build_configurations.each do |config| config.build_settings['OTHER_SWIFT_FLAGS'] ||= ['$(inherited)'] config.build_settings['OTHER_SWIFT_FLAGS'] << '-DADAPTY_KIDS_MODE' end end # highlight-end end end ``` 2. Run the following command to apply the changes: ```sh showLineNumbers title="Shell" pod install ``` #### Android: Enable Kids Mode using Gradle For Android, you can also enable Kids Mode at the native level by adding the following to your app's `build.gradle`: ```groovy showLineNumbers title="android/app/build.gradle" android { defaultConfig { // ... existing config ... // Enable Kids Mode buildConfigField "boolean", "ADAPTY_KIDS_MODE", "true" } } ``` ## Next steps Once you've enabled Kids Mode, make sure to: 1. Test your app thoroughly to ensure all functionality works correctly 2. Review your app's privacy policy to reflect the disabled data collection 3. Submit your app for review with clear documentation about Kids Mode compliance For more information about platform-specific requirements: - [Kids Mode in iOS SDK](kids-mode) for additional iOS configuration details - [Kids Mode in Android SDK](kids-mode-android) for additional Android configuration details --- # File: capacitor-reference --- --- title: "Reference" description: "Reference documentation for Adapty Capacitor SDK." --- This page contains reference documentation for Adapty Capacitor SDK. Choose the topic you need: - **[SDK models](https://capacitor.adapty.io/)** - Data models and structures used by the SDK - **[Handle errors](capacitor-handle-errors)** - Error handling and troubleshooting --- # File: capacitor-handle-errors --- --- title: "Handle errors in Capacitor SDK" description: "Handle errors in Capacitor SDK." --- Every error returned by the SDK is an `AdaptyError` instance. Here is an example: ```typescript showLineNumbers try { const result = await adapty.makePurchase({ product }); // Handle purchase result if (result.type === 'success') { console.log('Purchase successful:', result.profile); } else if (result.type === 'user_cancelled') { console.log('User cancelled the purchase'); } else if (result.type === 'pending') { console.log('Purchase is pending'); } } catch (error) { if (error instanceof AdaptyError) { console.error('Adapty error:', error.adaptyCode, error.localizedDescription); // Handle specific error codes switch (error.adaptyCode) { case ErrorCodeName.cantMakePayments: console.log('In-app purchases are not allowed on this device'); break; case ErrorCodeName.notActivated: console.log('Adapty SDK is not activated'); break; case ErrorCodeName.productPurchaseFailed: console.log('Purchase failed:', error.detail); break; default: console.log('Other error occurred:', error.detail); } } else { console.error('Non-Adapty error:', error); } } ``` ## Error Properties The `AdaptyError` class provides the following properties: | Property | Type | Description | |----------|------|-------------| | `adaptyCode` | `number` | Numeric error code (e.g., `1003` for cantMakePayments) | | `localizedDescription` | `string` | User-friendly error message | | `detail` | `string \| undefined` | Additional error details (optional) | | `message` | `string` | Full error message including code and description | ## Error Codes The SDK exports constants and utilities for working with error codes: ### ErrorCodeName Constant Maps string identifiers to numeric codes: ```typescript ErrorCodeName.cantMakePayments // 1003 ErrorCodeName.notActivated // 2002 ErrorCodeName.networkFailed // 2005 ``` ### ErrorCode Constant Maps numeric codes to string identifiers: ```typescript ErrorCode[1003] // 'cantMakePayments' ErrorCode[2002] // 'notActivated' ErrorCode[2005] // 'networkFailed' ``` ### Helper Functions ```typescript // Get numeric code from string name: getErrorCode('cantMakePayments') // 1003 // Get string name from numeric code: getErrorPrompt(1003) // 'cantMakePayments' ``` ### Comparing Error Codes **Important:** `error.adaptyCode` is a **number**, so compare it directly with numeric codes: ```typescript // Option 1: Use ErrorCodeName constant (recommended) ✅ if (error.adaptyCode === ErrorCodeName.cantMakePayments) { console.log('Cannot make payments'); } // Option 2: Compare with numeric literal ✅ if (error.adaptyCode === 1003) { console.log('Cannot make payments'); } // NOT like this ❌ - compares number to string and will never match if (error.adaptyCode === ErrorCode[1003]) { } ``` ## Global Error Handler You can set up a global error handler to catch all Adapty errors: ```typescript showLineNumbers // Set up global error handler AdaptyError.onError = (error: AdaptyError) => { console.error('Global Adapty error:', { code: error.adaptyCode, message: error.localizedDescription, detail: error.detail }); // Handle specific error types globally if (error.adaptyCode === ErrorCodeName.notActivated) { // SDK not activated - maybe retry activation console.log('SDK not activated, attempting to reactivate...'); } }; ``` ## Common Error Handling Patterns ### Handle Purchase Errors ```typescript showLineNumbers async function handlePurchase(product: AdaptyPaywallProduct) { try { const result = await adapty.makePurchase({ product }); if (result.type === 'success') { console.log('Purchase successful:', result.profile); } else if (result.type === 'user_cancelled') { console.log('User cancelled the purchase'); } else if (result.type === 'pending') { console.log('Purchase is pending'); } } catch (error) { if (error instanceof AdaptyError) { switch (error.adaptyCode) { case ErrorCodeName.cantMakePayments: console.log('In-app purchases not allowed'); break; case ErrorCodeName.productPurchaseFailed: console.log('Purchase failed:', error.detail); break; default: console.error('Purchase error:', error.localizedDescription); } } } } ``` ### Handle Network Errors ```typescript showLineNumbers async function fetchPaywall(placementId: string) { try { const paywall = await adapty.getPaywall({ placementId }); return paywall; } catch (error) { if (error instanceof AdaptyError) { switch (error.adaptyCode) { case ErrorCodeName.networkFailed: console.log('Network error, retrying...'); // Implement retry logic break; case ErrorCodeName.serverError: console.log('Server error:', error.detail); break; case ErrorCodeName.notActivated: console.log('SDK not activated'); break; default: console.error('Paywall fetch error:', error.localizedDescription); } } throw error; } } ``` ##  System StoreKit codes | Error | Code | Description | |-----|----|-----------| | unknown | 0 | This error indicates that an unknown or unexpected error occurred. | | clientInvalid | 1 | This error code indicates that the client is not allowed to perform the attempted action. | | paymentCancelled | 2 |

This error code indicates that the user canceled a payment request.

No action is required, but in terms of the business logic, you can offer a discount to your user or remind them later.

| | paymentInvalid | 3 | This error indicates that one of the payment parameters was not recognized by the store. | | paymentNotAllowed | 4 |

This error code indicates that the user is not allowed to authorize payments. Possible reasons:

- Payments are not supported in the user's country.

- The user is a minor.

| | storeProductNotAvailable | 5 | This error code indicates that the requested product is absent from the App Store. Make sure the product is available for the used country. | | cloudServicePermissionDenied | 6 | This error code indicates that the user has not allowed access to Cloud service information. | | cloudServiceNetworkConnectionFailed | 7 | This error code indicates that the device could not connect to the network. | | cloudServiceRevoked | 8 | This error code indicates that the user has revoked permission to use this cloud service. | | privacyAcknowledgementRequired | 9 | This error code indicates that the user has not yet acknowledged the store privacy policy. | | unauthorizedRequestData | 10 | This error code indicates that the request is built incorrectly. | | invalidOfferIdentifier | 11 |

The offer identifier is not valid. Possible reasons:

- You have not set up an offer with that identifier in the App Store.

- You have revoked the offer.

- You misprinted the offer ID.

| | invalidSignature | 12 | This error code indicates that the signature in a payment discount is not valid. Make sure you've filled out the **In-app purchase Key ID** field and uploaded the **In-App Purchase Private Key** file. Refer to the [Configure App Store integration](app-store-connection-configuration) topic for details. | | missingOfferParams | 13 |

This error indicates issues with Adapty integration or with offers.

Refer to the [Configure App Store integration](app-store-connection-configuration) and to [Offers](offers) for details on how to set them up.

| | invalidOfferPrice | 14 | This error code indicates that the price you specified in the store is no longer valid. Offers must always represent a discounted price. | ## Custom Android codes | Error | Code | Description | |-----|----|-----------| | adaptyNotInitialized | 20 | You need to properly configure Adapty SDK by `Adapty.activate` method. Learn how to do it [for React Native]( sdk-installation-reactnative#configure-adapty-sdks). | | productNotFound | 22 | This error indicates that the product requested for purchase is not available in the store. | | invalidJson | 23 | The paywall JSON is not valid. Fix it in the Adapty Dashboard. Refer to the [Customize paywall with remote config](customize-paywall-with-remote-config) topic for details on how to fix it. | | currentSubscriptionToUpdateNotFoundInHistory | 24 | The original subscription that needs to be renewed is not found. | | pendingPurchase | 25 | This error indicates that the purchase state is pending rather than purchased. Refer to the [Handling pending transactions](https://developer.android.com/google/play/billing/integrate#pending) page in the Android Developer docs for details. | | billingServiceTimeout | 97 | This error indicates that the request has reached the maximum timeout before Google Play can respond. This could be caused, for example, by a delay in the execution of the action requested by the Play Billing Library call. | | featureNotSupported | 98 | The requested feature is not supported by the Play Store on the current device. | | billingServiceDisconnected | 99 | This fatal error indicates that the client app’s connection to the Google Play Store service via the `BillingClient` has been severed. | | billingServiceUnavailable | 102 | This transient error indicates the Google Play Billing service is currently unavailable. In most cases, this means there is a network connection issue anywhere between the client device and Google Play Billing services. | | billingUnavailable | 103 |

This error indicates that a user billing error occurred during the purchase process. Examples of when this can occur include:

1\. The Play Store app on the user's device is out of date.

2. The user is in an unsupported country.

3. The user is an enterprise user, and their enterprise admin has disabled users from making purchases.

4. Google Play is unable to charge the user’s payment method. For example, the user's credit card might have expired.

5. The user is not logged into the Play Store app.

| | developerError | 105 | This is a fatal error that indicates you're improperly using an API. | | billingError | 106 | This is a fatal error that indicates an internal problem with Google Play itself. | | itemAlreadyOwned | 107 | The consumable product has already been purchased. | | itemNotOwned | 108 | This error indicates that the requested action on the item failed sin | ## Custom StoreKit codes | Error | Code | Description | |-----|----|-----------| | noProductIDsFound | 1000 |

This error indicates that none of the products in the paywall is available in the store.

If you are encountering this error, please follow the steps below to resolve it:

1. Check if all the products have been added to Adapty Dashboard.

2. Ensure that the Bundle ID of your app matches the one from the Apple Connect.

3. Verify that the product identifiers from the app stores match with the ones you have added to the Dashboard. Please note that the identifiers should not contain Bundle ID, unless it is already included in the store.

4. Confirm that the app paid status is active in your Apple tax settings. Ensure that your tax information is up-to-date and your certificates are valid.

5. Check if a bank account is attached to the app, so it can be eligible for monetization.

6. Check if the products are available in all regions.Also, ensure that your products are in **“Ready to Submit”** state.

| | productRequestFailed | 1002 |

Unable to fetch available products at the moment. Possible reason:

- No cache was yet created and no internet connection at the same time.

| | cantMakePayments | 1003 | In-App purchases are not allowed on this device. | | noPurchasesToRestore | 1004 | This error indicates that Google Play did not find the purchase to restore. | | cantReadReceipt | 1005 |

There is no valid receipt available on the device. This can be an issue during sandbox testing.

No action is required, but in terms of the business logic, you can offer a discount to your user or remind them later.

| | productPurchaseFailed | 1006 | Product purchase failed. | | refreshReceiptFailed | 1010 | This error indicates that the receipt was not received. Applicable to StoreKit 1 only. | | receiveRestoredTransactionsFailed | 1011 | Purchase restoration failed. | ## Custom network codes | Error | Code | Description | | :------------------- | :--- | :----------------------------------------------------------- | | notActivated | 2002 | You need to properly configure Adapty SDK by `Adapty.activate` method. Learn how to do it [for React Native](sdk-installation-reactnative#configure-adapty-sdks). | | badRequest | 2003 | Bad request. | | serverError | 2004 | Server error. | | networkFailed | 2005 | The network request failed. | | decodingFailed | 2006 | This error indicates that response decoding failed. | | encodingFailed | 2009 | This error indicates that request encoding failed. | | analyticsDisabled | 3000 | We can't handle analytics events, since you've opted it out. Refer to the [Analytics integration](analytics-integration) topic for details. | | wrongParam | 3001 | This error indicates that some of your parameters are not correct: blank when it cannot be blank or wrong type, etc. | | activateOnceError | 3005 | It is not possible to call `.activate` method more than once. | | profileWasChanged | 3006 | The user profile was changed during the operation. | | fetchTimeoutError | 3101 | This error means that the paywall could not be fetched within the set limit. To avoid this situation, [set up local fallbacks](fetch-paywalls-and-products). | | operationInterrupted | 9000 | This operation was interrupted by the system. | --- # File: capacitor-sdk-migration-guides --- --- title: "Capacitor SDK Migration Guides" description: "Migration guides for Adapty Capacitor SDK versions." --- This page contains all migration guides for Adapty Capacitor SDK. Choose the version you want to migrate to for detailed instructions: - [**Migrate to v. 3.16**](migration-to-capacitor-316.mdx) --- # File: migration-to-capacitor-316 --- --- title: "Migrate Adapty Capacitor SDK to v. 3.16" description: "Migrate to Adapty Capacitor SDK v3.16 for better performance and new monetization features." --- Starting from Adapty SDK v.3.16.0, Capacitor 8 is required. If you need Capacitor 7, use Adapty SDK v.3.15. To upgrade to Capacitor SDK v.3.16, ensure your project uses Capacitor 8. If you're still using Capacitor 7, you have two options: 1. **Upgrade to Capacitor 8**: Follow the [official Capacitor migration guide](https://capacitorjs.com/docs/updating/8-0) to update your project, then install Adapty SDK v.3.16. 2. **Stay on Adapty SDK v.3.15**: If upgrading to Capacitor 8 is not feasible, continue using Adapty SDK v.3.15, which supports Capacitor 7. --- # End of Documentation _Generated on: 2026-03-05T16:27:48.027Z_ _Successfully processed: 37/37 files_ # FLUTTER - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Generated on: 2026-03-05T16:27:48.029Z Total files: 42 --- # File: sdk-installation-flutter --- --- title: "Install & configure Flutter SDK" description: "Step-by-step guide on installing Adapty SDK on Flutter for subscription-based apps." --- Adapty SDK includes two key modules for seamless integration into your Flutter app: - **Core Adapty**: This essential SDK is required for Adapty to function properly in your app. - **AdaptyUI**: This optional module is needed if you use the [Adapty Paywall Builder](adapty-paywall-builder), a user-friendly, no-code tool for easily creating cross-platform paywalls. :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample app](https://github.com/adaptyteam/AdaptySDK-Flutter/tree/master/example), which demonstrates the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ## Requirements Adapty SDK supports iOS 13.0+, but requires iOS 15.0+ to work properly with paywalls created in the paywall builder. :::info Adapty is compatible with Google Play Billing Library up to 8.x. By default, Adapty works with Google Play Billing Library v.7.0.0 but, if you want to force a later version, you can manually [add the dependency](https://developer.android.com/google/play/billing/integrate#dependency). ::: ## Install Adapty SDK [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-Flutter.svg?style=flat&logo=flutter)](https://github.com/adaptyteam/AdaptySDK-Flutter/releases) 1. Add Adapty to your `pubspec.yaml` file: ```yaml showLineNumbers title="pubspec.yaml" dependencies: adapty_flutter: ^ ``` 2. Run the following command to install dependencies: ```bash showLineNumbers title="Terminal" flutter pub get ``` 3. Import Adapty SDKs in your application: ```dart showLineNumbers title="main.dart" import 'package:adapty_flutter/adapty_flutter.dart'; ``` ## Activate Adapty module of Adapty SDK Activate the Adapty SDK in your app code. :::note The Adapty SDK only needs to be activated once in your app. ::: To get your **Public SDK Key**: 1. Go to Adapty Dashboard and navigate to [**App settings → General**](https://app.adapty.io/settings/general). 2. From the **Api keys** section, copy the **Public SDK Key** (NOT the Secret Key). 3. Replace `"YOUR_PUBLIC_SDK_KEY"` in the code. :::important - Make sure you use the **Public SDK key** for Adapty initialization, the **Secret key** should be used for [server-side API](getting-started-with-server-side-api) only. - **SDK keys** are unique for every app, so if you have multiple apps make sure you choose the right one. ::: ```dart showLineNumbers title="main.dart" void main() { runApp(MyApp()); } class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State { @override void initState() { _initializeAdapty(); super.initState(); } Future _initializeAdapty() async { try { await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY'), ); } catch (e) { // handle the error } } Widget build(BuildContext context) { return Text("Hello"); } } ``` Now set up paywalls in your app: - If you use [Adapty Paywall Builder](adapty-paywall-builder), first [activate the AdaptyUI module](#activate-adaptyui-module-of-adapty-sdk) below, then follow the [Paywall Builder quickstart](flutter-quickstart-paywalls). - If you build your own paywall UI, see the [quickstart for custom paywalls](flutter-quickstart-manual). ## Activate AdaptyUI module of Adapty SDK If you plan to use [Paywall Builder](adapty-paywall-builder.md) and have [installed AdaptyUI module](sdk-installation-flutter#install-adapty-sdk), you also need to activate AdaptyUI: :::important In your code, you must activate the core Adapty module before activating AdaptyUI. ::: ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withActivateUI(true), // This automatically activates AdaptyUI ); ``` ## Platform-specific configuration Create `Adapty-Info.plist` and add it to your iOS project. Add the following configuration: ```xml showLineNumbers title="ios/Runner/Adapty-Info.plist" AdaptyPublicSdkKey YOUR_PUBLIC_SDK_KEY AdaptyObserverMode AdaptyAppleIdfaCollectionDisabled ``` Parameters: | Parameter | Presence | Description | |---------|--------|-----------| | **AdaptyPublicSdkKey** | required | The key you can find in the **Public SDK key** field of your app settings in Adapty: [**App settings**-> **General** tab -> **API keys** subsection](https://app.adapty.io/settings/general) | | **AdaptyObserverMode** | optional | A boolean value controlling [Observer mode](observer-vs-full-mode). Turn it on if you handle purchases and subscription status yourself and use Adapty for sending subscription events and analytics. The default value is `false`. 🚧 When running in Observer mode, Adapty SDK won't close any transactions, so make sure you're handling it. | | **AdaptyAppleIdfaCollectionDisabled** | optional | A boolean parameter that allows you to disable Apple IDFA collection for your iOS app. The default value is `false`. For more details, refer to the [Analytics integration](analytics-integration#disable-collection-of-advertising-identifiers) section. | Add the following configuration to your `AndroidManifest.xml`: ```xml showLineNumbers title="android/app/src/main/AndroidManifest.xml" ``` Parameters: | Parameter | Presence | Description | |---------|--------|-----------| | **AdaptyPublicSdkKey** | required | The key you can find in the **Public SDK key** field of your app settings in Adapty: [**App settings**-> **General** tab -> **API keys** subsection](https://app.adapty.io/settings/general). Make sure you use the **Public SDK key** for Adapty initialization, the **Secret key** should be used for [server-side API](getting-started-with-server-side-api) only. | | **AdaptyObserverMode** | optional | A boolean value that controls [Observer mode](observer-vs-full-mode). Turn it on if you handle purchases and subscription status yourself and use Adapty for sending subscription events and analytics. The default value is `false`. 🚧 When running in Observer mode, Adapty SDK won't close any transactions, so make sure you're handling it. | | **AdaptyGoogleAdvertisingIdCollectionDisabled** | optional | A boolean parameter that allows you to disable Google Advertising ID collection for your app. The default value is `false`. For more details, refer to the [Analytics integration](analytics-integration#disable-collection-of-advertising-identifiers) section. | ## Optional setup ### Logging #### Set up the logging system Adapty logs errors and other important information to help you understand what is going on. There are the following levels available: | Level | Description | | :----------------------- | :------------------------------------------------------------------------------------------------------------------------ | | `AdaptyLogLevel.none` | Nothing will be logged. Default value | | `AdaptyLogLevel.error` | Only errors will be logged | | `AdaptyLogLevel.warn` | Errors and messages from the SDK that do not cause critical errors, but are worth paying attention to will be logged. | | `AdaptyLogLevel.info` | Errors, warnings, and various information messages will be logged. | | `AdaptyLogLevel.verbose` | Any additional information that may be useful during debugging, such as function calls, API queries, etc. will be logged. | You can set the log level in your app before configuring Adapty: ```dart showLineNumbers title="main.dart" // Set log level before activation. // 'verbose' is recommended for development and the first production release await Adapty().setLogLevel(AdaptyLogLevel.verbose); // Or set it during configuration await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withLogLevel(AdaptyLogLevel.verbose), ); ``` ### Data policies Adapty doesn't store personal data of your users unless you explicitly send it, but you can implement additional data security policies to comply with the store or country guidelines. #### Disable IP address collection and sharing When activating the Adapty module, set `ipAddressCollectionDisabled` to `true` to disable user IP address collection and sharing. The default value is `false`. Use this parameter to enhance user privacy, comply with regional data protection regulations (like GDPR or CCPA), or reduce unnecessary data collection when IP-based features aren't required for your app. ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withIpAddressCollectionDisabled(true), ); ``` #### Disable advertising ID collection and sharing When activating the Adapty module, set `appleIdfaCollectionDisabled` (iOS) or `googleAdvertisingIdCollectionDisabled` (Android) to `true` to disable the collection of advertising identifiers. The default value is `false`. Use this parameter to comply with App Store/Play Store policies, avoid triggering the App Tracking Transparency prompt, or if your app does not require advertising attribution or analytics based on advertising IDs. ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withAppleIdfaCollectionDisabled(true) // iOS ..withGoogleAdvertisingIdCollectionDisabled(true), // Android ); ``` #### Set up media cache configuration for AdaptyUI The module is activated automatically with the Adapty SDK. If you do not use the Paywall Builder and want to deactivate the AdaptyUI module, pass `withActivateUI(false)` during activation. By default, AdaptyUI caches media (such as images and videos) to improve performance and reduce network usage. You can customize the cache settings by providing a custom configuration. Use `withMediaCacheConfiguration` to override the default cache size and validity period. This is optional—if you don't call this method, default values will be used (100MB disk size, unlimited memory count). However, if you use the configuration, all parameters must be included. ```dart showLineNumbers title="main.dart" final mediaCacheConfig = AdaptyUIMediaCacheConfiguration( memoryStorageTotalCostLimit: 200 * 1024 * 1024, // 200 MB memoryStorageCountLimit: 2147483647, // max int value diskStorageSizeLimit: 200 * 1024 * 1024, // 200 MB ); await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withMediaCacheConfiguration(mediaCacheConfig), ); ``` **Parameters:** | Parameter | Presence | Description | |-------------------------|----------|-----------------------------------------------------------------------------| | memoryStorageTotalCostLimit | optional | Total cache size in memory in bytes. Default is 100 MB. | | memoryStorageCountLimit | optional | The item count limit of the memory storage. Default is max int value. | | diskStorageSizeLimit | optional | The file size limit on disk in bytes. Default is 100 MB. | ### Enable local access levels (Android) By default, [local access levels](local-access-levels.md) are enabled on iOS and disabled on Android. To enable them on Android as well, set `withGoogleLocalAccessLevelAllowed` to `true`: ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withGoogleLocalAccessLevelAllowed(true), ); ``` ### Clear data on backup restore When `clearDataOnBackup` is set to `true`, the SDK detects when the app is restored from an iCloud backup and deletes all locally stored SDK data, including cached profile information, product details, and paywalls. The SDK then initializes with a clean state. Default value is `false`. :::note Only local SDK cache is deleted. Transaction history with Apple and user data on Adapty servers remain unchanged. ::: ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withClearDataOnBackup(true) // default – false ); ``` ## Troubleshooting #### Android backup rules (Auto Backup configuration) Some SDKs (including Adapty) ship their own Android Auto Backup configuration. If you use multiple SDKs that define backup rules, the Android manifest merger can fail with an error mentioning `android:fullBackupContent`, `android:dataExtractionRules`, or `android:allowBackup`. Typical error symptoms: `Manifest merger failed: Attribute application@dataExtractionRules value=(@xml/your_data_extraction_rules) is also present at [com.other.sdk:library:1.0.0] value=(@xml/other_sdk_data_extraction_rules)` :::note These changes should be made in your Android platform directory (typically located in your project's `android/` folder). ::: To resolve this, you need to: - Tell the manifest merger to use your app's values for backup-related attributes. - Create backup rule files that merge Adapty's rules with rules from other SDKs. #### 1. Add the `tools` namespace to your manifest In your `AndroidManifest.xml` file, ensure the root `` tag includes tools: ```xml ... ``` #### 2. Override backup attributes in `` In the same `AndroidManifest.xml` file, update the `` tag so that your app provides the final values and tells the manifest merger to replace library values: ```xml ... ``` If any SDK also sets `android:allowBackup`, include it in `tools:replace` as well: ```xml tools:replace="android:allowBackup,android:fullBackupContent,android:dataExtractionRules" ``` #### 3. Create merged backup rules files Create XML files in your Android project's `res/xml/` directory that combine Adapty's rules with rules from other SDKs. Android uses different backup rule formats depending on the OS version, so creating both files ensures compatibility across all Android versions your app supports. :::note The examples below show AppsFlyer as a sample third-party SDK. Replace or add rules for any other SDKs you're using in your app. ::: **For Android 12 and higher** (uses the new data extraction rules format): ```xml title="sample_data_extraction_rules.xml" ``` **For Android 11 and lower** (uses the legacy full backup content format): ```xml title="sample_backup_rules.xml" #### Purchases fail after returning from another app in Android If the Activity that starts the purchase flow uses a non-default `launchMode`, Android may recreate or reuse it incorrectly when the user returns from Google Play, a banking app, or a browser. This can cause the purchase result to be lost or treated as canceled. To ensure purchases work correctly, use only `standard` or `singleTop` launch modes for the Activity that starts the purchase flow, and avoid any other modes. In your `AndroidManifest.xml`, ensure the Activity that starts the purchase flow is set to `standard` or `singleTop`: ```xml ``` --- # File: flutter-quickstart-paywalls --- --- title: "Enable purchases by using paywalls in Flutter SDK" description: "Quickstart guide to setting up Adapty for in-app subscription management." --- To enable in-app purchases, you need to understand three key concepts: - [**Products**](product.md) – anything users can buy (subscriptions, consumables, lifetime access) - [**Paywalls**](paywalls.md) are configurations that define which products to offer. In Adapty, paywalls are the only way to retrieve products, but this design lets you modify offerings, pricing, and product combinations without touching your app code. - [**Placements**](placements.md) – where and when you show paywalls in your app (like `main`, `onboarding`, `settings`). You set up paywalls for placements in the dashboard, then request them by placement ID in your code. This makes it easy to run A/B tests and show different paywalls to different users. Adapty offers you three ways to enable purchases in your app. Select one of them depending on your app requirements: | Implementation | Complexity | When to use | |------------------------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Adapty Paywall Builder | ✅ Easy | You [create a complete, purchase-ready paywall in the no-code builder](quickstart-paywalls). Adapty automatically renders it and handles all the complex purchase flow, receipt validation, and subscription management behind the scenes. | | Manually created paywalls | 🟡 Medium | You implement your paywall UI in your app code, but still get the paywall object from Adapty to maintain flexibility in product offerings. See the [guide](flutter-quickstart-manual). | | Observer mode | 🔴 Hard | You already have your own purchase handling infrastructure and want to keep using it. Note that the observer mode has its limitations in Adapty. See the [article](observer-vs-full-mode). | :::important **The steps below show how to implement a paywall created in the Adapty paywall builder.** If you don't want to use the paywall builder, see the [guide for handling purchases in manually created paywalls](flutter-making-purchases.md). ::: To display a paywall created in the Adapty paywall builder, in your app code, you only need to: 1. **Get the paywall**: Get the paywall from Adapty. 2. **Display the paywall and Adapty will handle purchases for you**: Show the paywall container you've got in your app. 3. **Handle button actions**: Associate user interactions with the paywall with your app's response to them. For example, open links or close the paywall when users click buttons. ## Before you start Before you start, complete these steps: 1. Connect your app to the [App Store](initial_ios) and/or [Google Play](initial-android) in the Adapty Dashboard. 2. [Create your products](create-product) in Adapty. 3. [Create a paywall and add products to it](create-paywall). 4. [Create a placement and add your paywall to it](create-placement). 5. [Install and activate the Adapty SDK](sdk-installation-flutter) in your app code. :::tip The fastest way to complete these steps is to follow the [quickstart guide](quickstart). ::: ## 1. Get the paywall Your paywalls are associated with placements configured in the dashboard. Placements allow you to run different paywalls for different audiences or to run [A/B tests](ab-tests.md). To get a paywall created in the Adapty paywall builder, you need to: 1. Get the `paywall` object by the [placement](placements.md) ID using the `getPaywall` method and check whether it is a paywall created in the builder using the `hasViewConfiguration` property. 2. Create the paywall view using the `createPaywallView` method. The view contains the UI elements and styling needed to display the paywall. :::important To get the view configuration, you must switch on the **Show on device** toggle in the Paywall Builder. Otherwise, you will get an empty view configuration, and the paywall won't be displayed. ::: ```dart showLineNumbers try { final paywall = await Adapty().getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en"); // the requested paywall } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } try { final view = await AdaptyUI().createPaywallView( paywall: paywall, ); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ## 2. Display the paywall Now, when you have the paywall configuration, it's enough to add a few lines to display your paywall. To display the paywall, use the `view.present()` method on the `view` created by the `createPaywallView` method. Each `view` can only be used once. If you need to display the paywall again, call `createPaywallView` one more to create a new `view` instance. ```dart showLineNumbers title="Flutter" try { await view.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` :::tip For more details on how to display a paywall, see our [guide](flutter-present-paywalls.md). ::: ## 3. Handle button actions When users click buttons in the paywall, the Flutter SDK automatically handles purchases and restoration. However, other buttons have custom or pre-defined IDs and require handling actions in your code. To control or monitor processes on the paywall screen, implement the `AdaptyUIPaywallsEventsObserver` methods and set the observer before presenting any screen. If a user has performed some action, te `paywallViewDidPerformAction` will be invoked, and your app needs to respond depending on the action ID. For example, your paywall probably has a close button and URLs to open (e.g., terms of use and privacy policy). So, you need to respond to actions with the `Close` and `OpenUrl` IDs. :::tip Read our guides on how to handle button [actions](flutter-handle-paywall-actions.md) and [events](flutter-handling-events.md). ::: ```dart showLineNumbers title="Flutter" class _PaywallScreenState extends State implements AdaptyUIPaywallsEventsObserver { @override void initState() { super.initState(); // Register this class as the paywalls event observer AdaptyUI().setPaywallsEventsObserver(this); } // This method is called when user performs an action on the paywall UI @override void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) { switch (action) { case const CloseAction(): case const AndroidSystemBackAction(): view.dismiss(); break; case OpenUrlAction(url: final url): // Open the URL using url_launcher package _launchUrl(url); break; } } // Helper method to launch URLs Future _launchUrl(String url) async { try { final Uri uri = Uri.parse(url); if (await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); } else { // Handle case where URL cannot be launched print('Could not launch $url'); } } catch (e) { // Handle any errors print('Error launching URL: $e'); } } } ``` ## Next steps Your paywall is ready to be displayed in the app. Test your purchases in the [App Store sandbox](test-purchases-in-sandbox) or in [Google Play Store](testing-on-android) to make sure you can complete a test purchase from the paywall. Now, you need to [check the users' access level](flutter-check-subscription-status.md) to ensure you display a paywall or give access to paid features to right users. ## Full example Here is how all those steps can be integrated in your app together. ```dart void main() async { runApp(MaterialApp(home: PaywallScreen())); } class PaywallScreen extends StatefulWidget { @override State createState() => _PaywallScreenState(); } class _PaywallScreenState extends State implements AdaptyUIPaywallsEventsObserver { @override void initState() { super.initState(); // Register this class as the paywalls event observer AdaptyUI().setPaywallsEventsObserver(this); _showPaywallIfNeeded(); } Future _showPaywallIfNeeded() async { try { final paywall = await Adapty().getPaywall( placementId: 'YOUR_PLACEMENT_ID', ); if (!paywall.hasViewConfiguration) return; final view = await AdaptyUI().createPaywallView(paywall: paywall); await view.present(); } catch (_) { // Handle any errors (network, SDK issues, etc.) } } // This method is called when user performs an action on the paywall UI @override void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) { switch (action) { case const CloseAction(): case const AndroidSystemBackAction(): view.dismiss(); break; case OpenUrlAction(url: final url): // Open the URL using url_launcher package _launchUrl(url); break; } } // Helper method to launch URLs Future _launchUrl(String url) async { try { final Uri uri = Uri.parse(url); if (await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); } else { // Handle case where URL cannot be launched print('Could not launch $url'); } } catch (e) { // Handle any errors print('Error launching URL: $e'); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Adapty Paywall Example')), body: Center( // Add a button to re-trigger the paywall for testing purposes child: ElevatedButton( onPressed: _showPaywallIfNeeded, child: Text('Show Paywall'), ), ), ); } } ``` --- # File: flutter-check-subscription-status --- --- title: "Check subscription status in Flutter SDK" description: "Learn how to check subscription status in your Flutter app with Adapty." --- To decide whether users can access paid content or see a paywall, you need to check their [access level](access-level.md) in the profile. This article shows you how to access the profile state to decide what users need to see - whether to show them a paywall or grant access to paid features. ## Get subscription status When you decide whether to show a paywall or paid content to a user, you check their [access level](access-level.md) in their profile. You have two options: - Call `getProfile` if you need the latest profile data immediately (like on app launch) or want to force an update. - Set up **automatic profile updates** to keep a local copy that's automatically refreshed whenever the subscription status changes. ### Get profile The easiest way to get the subscription status is to use the `getProfile` method to access the profile: ```javascript showLineNumbers try { final profile = await Adapty().getProfile(); // check the access } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ### Listen to subscription updates To automatically receive profile updates in your app: 1. Use `Adapty().didUpdateProfileStream.listen()` to listen for profile changes - Adapty will automatically call this method whenever the user's subscription status changes. 2. Store the updated profile data when this method is called, so you can use it throughout your app without making additional network requests. ```dart class SubscriptionManager { AdaptyProfile? _currentProfile; SubscriptionManager() { // Listen for profile updates Adapty().didUpdateProfileStream.listen((profile) { _currentProfile = profile; // Update UI, unlock content, etc. }); } // Use stored profile instead of calling getProfile() bool hasAccess() { return _currentProfile?.accessLevels['premium']?.isActive ?? false; } } ``` :::note Adapty automatically calls the profile update stream listener when your app starts, providing cached subscription data even if the device is offline. ::: ## Connect profile with paywall logic When you need to make immediate decisions about showing paywalls or granting access to paid features, you can check the user's profile directly. This approach is useful for scenarios like app launch, when entering premium sections, or before displaying specific content. ```dart Future _checkAccessLevel() async { try { final profile = await Adapty().getProfile(); return profile.accessLevels['YOUR_ACCESS_LEVEL']?.isActive ?? false; } catch (e) { print('Error checking access level: $e'); return false; // Show paywall if access check fails } } Future _initializePaywall() async { await _loadPaywall(); final hasAccess = await _checkAccessLevel(); if (!hasAccess) { // Show paywall if no access } } ``` ## Next steps Now, when you know how to track the subscription status, learn how to [work with user profiles](flutter-quickstart-identify.md) to ensure they can access what they have paid for. --- # File: flutter-quickstart-identify --- --- title: "Identify users in Flutter SDK" description: "Quickstart guide to setting up Adapty for in-app subscription management in Flutter." --- :::important This guide is for you if you have your own authentication system. Here, you will learn how to work with user profiles in Adapty to ensure it aligns with your existing authentication system. ::: How you manage users' purchases depends on your app's authentication model: - If your app doesn't use backend authentication and doesn't store user data, see the [section about anonymous users](#anonymous-users). - If your app has (or will have) backend authentication, see the [section about identified users](#identified-users). **Key concepts**: - **Profiles** are the entities required for the SDK to work. Adapty creates them automatically. - They can be anonymous **(without customer user ID)** or identified **(with customer user ID)**. - You provide **customer user ID** in order to cross-reference profiles in Adapty with your internal auth system Here is what is different for anonymous and identified users: | | Anonymous users | Identified users | |-------------------------|---------------------------------------------------|-------------------------------------------------------------------------| | **Purchase management** | Store-level purchase restoration | Maintain purchase history across devices through their customer user ID | | **Profile management** | New profiles on each reinstall | The same profile across sessions and devices | | **Data persistence** | Anonymous users' data is tied to app installation | Identified users' data persists across app installations | ## Anonymous users If you don't have backend authentication, **you don't need to handle authentication in the app code**: 1. When the SDK is activated on the app's first launch, Adapty **creates a new profile for the user**. 2. When the user purchases anything in the app, this purchase is **associated with their Adapty profile and their store account**. 3. When the user **re-installs** the app or installs it from a **new device**, Adapty **creates a new anonymous profile on activation**. 4. If the user has previously made purchases in your app, by default, their purchases are automatically synced from the App Store on the SDK activation. So, with anonymous users, new profiles will be created on each installation, but that's not a problem because, in the Adapty analytics, you can [configure what will be considered a new installation](general#4-installs-definition-for-analytics). For anonymous users, you need to count installs by **device IDs**. In this case, each app installation on a device is counted as an install, including reinstalls. ## Identified users You have two options to identify users in the app: - [**During login/signup:**](#during-loginsignup) If users sign in after your app starts, call `identify()` with a customer user ID when they authenticate. - [**During the SDK activation:**](#during-the-sdk-activation) If you already have a customer user ID stored when the app launches, send it when calling `activate()`. :::important By default, when Adapty receives a purchase from a Customer User ID that is currently associated with another Customer User ID, the access level is shared, so both profiles have paid access. You can configure this setting to transfer paid access from one profile to another or disable sharing completely. See the [article](general#6-sharing-purchases-between-user-accounts) for more details. ::: ### During login/signup If you're identifying users after the app launch (for example, after they log into your app or sign up), use the `identify` method to set their customer user ID. - If you **haven't used this customer user ID before**, Adapty will automatically link it to the current profile. - If you **have used this customer user ID to identify the user before**, Adapty will switch to working with the profile associated with this customer user ID. :::important Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one. ::: ```dart showLineNumbers try { await Adapty().identify(customerUserId); // Unique for each user } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ### During the SDK activation If you already know a customer user ID when you activate the SDK, you can send it in the `activate` method instead of calling `identify` separately. If you know a customer user ID but set it only after the activation, that will mean that, upon activation, Adapty will create a new anonymous profile and switch to the existing one only after you call `identify`. You can pass either an existing customer user ID (the one you have used before) or a new one. If you pass a new one, a new profile created upon activation will be automatically linked to the customer user ID. :::note By default, creating anonymous profiles does not affect analytics dashboards, because installs are counted based on device IDs. A device ID represents a single installation of the app from the store on a device and is regenerated only after the app is reinstalled. It does not depend on whether this is a first or repeated installation, or whether an existing customer user ID is used. Creating a profile (on SDK activation or logout), logging in, or upgrading the app without reinstalling the app does not generate additional install events. If you want to count installs based on unique users rather than devices, go to **App settings** and configure [**Installs definition for analytics**](general#4-installs-definition-for-analytics). ::: ```dart showLineNumbers" try { await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_API_KEY') ..withCustomerUserId(YOUR_CUSTOMER_USER_ID) // Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one. ); } catch (e) { // handle the error } ``` ### Log users out If you have a button for logging users out, use the `logout` method. :::important Logging users out creates a new anonymous profile for the user. ::: ```dart showLineNumbers try { await Adapty().logout(); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle unknown error } ``` :::info To log users back into the app, use the `identify` method. ::: ### Allow purchases without login If your users can make purchases both before and after they log into your app, you need to ensure that they will keep access after they log in: 1. When a logged-out user makes a purchase, Adapty ties it to their anonymous profile ID. 2. When the user logs into their account, Adapty switches to working with their identified profile. - If it is a new customer user ID (e.g., the purchase has been made before registration), Adapty assigns the customer user ID to the current profile, so all the purchase history is maintained. - If it is an existing customer user ID (the customer user ID is already linked to a profile), you need to get the actual access level after the profile switch. You can either call [`getProfile`](flutter-check-subscription-status.md) right after the identification, or [listen for profile updates](flutter-check-subscription-status.md) so the data syncs automatically. ## Next steps Congratulations! You have implemented in-app payment logic in your app! We wish you all the best with your app monetization! To get even more from Adapty, you can explore these topics: - [**Testing**](troubleshooting-test-purchases.md): Ensure that everything works as expected - [**Onboardings**](flutter-onboardings.md): Engage users with onboardings and drive retention - [**Integrations**](configuration.md): Integrate with marketing attribution and analytics services in just one line of code - [**Set custom profile attributes**](flutter-setting-user-attributes.md): Add custom attributes to user profiles and create segments, so you can launch A/B tests or show different paywalls to different users --- # File: adapty-cursor-flutter --- --- title: "Integrate Adapty into your Flutter app with AI assistance" description: "A step-by-step guide to integrating Adapty into your Flutter app using Cursor, Context7, ChatGPT, Claude, or other AI tools." --- This guide helps you integrate Adapty into your Flutter app with the help of an LLM. You'll start by preparing your Adapty dashboard, then work through each implementation stage by sending focused doc links to your LLM. At the end, you'll find best practices for setting up your AI tools with Adapty documentation. :::tip Copy this entire page as Markdown and paste it into your LLM to get started — click **Copy for LLM** at the top of the page or open [the .md version](https://adapty.io/docs/adapty-cursor-flutter.md). The LLM will use the guide links and checkpoints to walk you through each stage. ::: ## Before you start: dashboard checklist Adapty requires dashboard configuration before you write any SDK code. Your LLM cannot look up dashboard values for you — you'll need to provide them. ### Required before coding 1. **Connect your app stores**: In the Adapty Dashboard, go to **App settings → General**. Connect both App Store and Google Play if your Flutter app targets both platforms. This is required for purchases to work. [Connect app stores](integrate-payments.md) 2. **Copy your Public SDK key**: In the Adapty Dashboard, go to **App settings → General**, then find the **API keys** section. In code, this is the string you pass to the Adapty configuration. 3. **Create at least one product**: In the Adapty Dashboard, go to the **Products** page. You don't reference products directly in code — Adapty delivers them through paywalls. [Add products](quickstart-products.md) 4. **Create a paywall and a placement**: In the Adapty Dashboard, create a paywall on the **Paywalls** page, then assign it to a placement on the **Placements** page. In code, the placement ID is the string you pass to `Adapty().getPaywall()`. [Create paywall](quickstart-paywalls.md) 5. **Set up access levels**: In the Adapty Dashboard, configure per product on the **Products** page. In code, the string checked in `profile.accessLevels['premium']?.isActive`. The default `premium` access level works for most apps. If paying users get access to different features depending on the product (for example, a `basic` plan vs. a `pro` plan), [create additional access levels](assigning-access-level-to-a-product.md) before you start coding. :::tip Once you have all five, you're ready to write code. Tell your LLM: "My Public SDK key is X, my placement ID is Y" so it can generate correct initialization and paywall-fetching code. ::: ### Set up when ready These are not required to start coding, but you'll want them as your integration matures: - **A/B tests**: Configure on the **Placements** page. No code change needed. [A/B tests](ab-tests.md) - **Additional paywalls and placements**: Add more `getPaywall` calls with different placement IDs. - **Analytics integrations**: Configure on the **Integrations** page. Setup varies by integration. See [analytics integrations](analytics-integration.md) and [attribution integrations](attribution-integration.md). ## Feed Adapty docs to your LLM ### Use Context7 (recommended) [Context7](https://context7.com) is an MCP server that gives your LLM direct access to up-to-date Adapty documentation. Your LLM fetches the right docs automatically based on what you ask — no manual URL pasting needed. Context7 works with **Cursor**, **Claude Code**, **Windsurf**, and other MCP-compatible tools. To set it up, run: ``` npx ctx7 setup ``` This detects your editor and configures the Context7 server. For manual setup, see the [Context7 GitHub repository](https://github.com/upstash/context7). Once configured, reference the Adapty library in your prompts: ``` Use the adaptyteam/adapty-docs library to look up how to install the Flutter SDK ``` :::warning Even though Context7 removes the need to paste doc links manually, the implementation order matters. Follow the [implementation walkthrough](#implementation-walkthrough) below step by step to make sure everything works. ::: ### Use plain text docs You can access any Adapty doc as plain text Markdown. Add `.md` to the end of its URL, or click **Copy for LLM** under the article title. For example: [adapty-cursor-flutter.md](https://adapty.io/docs/adapty-cursor-flutter.md). Each stage in the [implementation walkthrough](#implementation-walkthrough) below includes a "Send this to your LLM" block with `.md` links to paste. For more documentation at once, see [index files and platform-specific subsets](#plain-text-doc-index-files) below. ## Implementation walkthrough The rest of this guide walks through Adapty integration in implementation order. Each stage includes the docs to send to your LLM, what you should see when done, and common issues. ### Plan your integration Before jumping into code, ask your LLM to analyze your project and create an implementation plan. If your AI tool supports a planning mode (like Cursor's or Claude Code's plan mode), use it so the LLM can read both your project structure and the Adapty docs before writing any code. Tell your LLM which approach you use for purchases — this affects the guides it should follow: - [**Adapty Paywall Builder**](adapty-paywall-builder.md): You create paywalls in Adapty's no-code builder, and the SDK renders them automatically. - [**Manually created paywalls**](flutter-making-purchases.md): You build your own paywall UI in code but still use Adapty to fetch products and handle purchases. - [**Observer mode**](observer-vs-full-mode.md): You keep your existing purchase infrastructure and use Adapty only for analytics and integrations. Not sure which one to pick? Read the [comparison table in the quickstart](flutter-quickstart-paywalls.md). ### Install and configure the SDK Add the Adapty SDK dependency using `flutter pub add` and activate it with your Public SDK key. This is the foundation — nothing else works without it. **Guide:** [Install & configure Adapty SDK](sdk-installation-flutter.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/sdk-installation-flutter.md ``` :::tip[Checkpoint] - **Expected:** App builds and runs on both iOS and Android. Debug console shows Adapty activation log. - **Gotcha:** "Public API key is missing" → check you replaced the placeholder with your real key from App settings. ::: ### Show paywalls and handle purchases Fetch a paywall by placement ID, display it, and handle purchase events. The guides you need depend on how you handle purchases. Test each purchase in the sandbox as you go — don't wait until the end. See [Test purchases in sandbox](test-purchases-in-sandbox.md) for setup instructions. **Guides:** - [Enable purchases using paywalls (quickstart)](flutter-quickstart-paywalls.md) - [Fetch Paywall Builder paywalls and their configuration](flutter-get-pb-paywalls.md) - [Display paywalls](flutter-present-paywalls.md) - [Handle paywall events](flutter-handling-events.md) - [Respond to button actions](flutter-handle-paywall-actions.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/flutter-quickstart-paywalls.md - https://adapty.io/docs/flutter-get-pb-paywalls.md - https://adapty.io/docs/flutter-present-paywalls.md - https://adapty.io/docs/flutter-handling-events.md - https://adapty.io/docs/flutter-handle-paywall-actions.md ``` :::tip[Checkpoint] - **Expected:** Paywall appears with your configured products. Tapping a product triggers the sandbox purchase dialog. - **Gotcha:** Empty paywall or `getPaywall` error → verify placement ID matches the dashboard exactly and the placement has an audience assigned. ::: **Guides:** - [Enable purchases in your custom paywall (quickstart)](flutter-quickstart-manual.md) - [Fetch paywalls and products](fetch-paywalls-and-products-flutter.md) - [Render paywall designed by remote config](present-remote-config-paywalls-flutter.md) - [Make purchases](flutter-making-purchases.md) - [Restore purchases](flutter-restore-purchase.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/flutter-quickstart-manual.md - https://adapty.io/docs/fetch-paywalls-and-products-flutter.md - https://adapty.io/docs/present-remote-config-paywalls-flutter.md - https://adapty.io/docs/flutter-making-purchases.md - https://adapty.io/docs/flutter-restore-purchase.md ``` :::tip[Checkpoint] - **Expected:** Your custom paywall displays products fetched from Adapty. Tapping a product triggers the sandbox purchase dialog. - **Gotcha:** Empty products array → verify the paywall has products assigned in the dashboard and the placement has an audience. ::: **Guides:** - [Observer mode overview](observer-vs-full-mode.md) - [Implement Observer mode](implement-observer-mode-flutter.md) - [Report transactions in Observer mode](report-transactions-observer-mode-flutter.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/observer-vs-full-mode.md - https://adapty.io/docs/implement-observer-mode-flutter.md - https://adapty.io/docs/report-transactions-observer-mode-flutter.md ``` :::tip[Checkpoint] - **Expected:** After a sandbox purchase using your existing purchase flow, the transaction appears in the Adapty dashboard **Event Feed**. - **Gotcha:** No events → verify you're reporting transactions to Adapty and server notifications are configured for both stores. ::: ### Check subscription status After a purchase, check the user profile for an active access level to gate premium content. **Guide:** [Check subscription status](flutter-check-subscription-status.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/flutter-check-subscription-status.md ``` :::tip[Checkpoint] - **Expected:** After a sandbox purchase, `profile.accessLevels['premium']?.isActive` returns `true`. - **Gotcha:** Empty `accessLevels` after purchase → check the product has an access level assigned in the dashboard. ::: ### Identify users Link your app user accounts to Adapty profiles so purchases persist across devices. :::important Skip this step if your app has no authentication. ::: **Guide:** [Identify users](flutter-quickstart-identify.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/flutter-quickstart-identify.md ``` :::tip[Checkpoint] - **Expected:** After calling `Adapty().identify()`, the dashboard **Profiles** section shows your custom user ID. - **Gotcha:** Call `identify` after activation but before fetching paywalls to avoid anonymous profile attribution. ::: ### Prepare for release Once your integration works in the sandbox, walk through the release checklist to make sure everything is production-ready. **Guide:** [Release checklist](release-checklist.md) Send this to your LLM: ``` Read these Adapty docs before releasing: - https://adapty.io/docs/release-checklist.md ``` :::tip[Checkpoint] - **Expected:** All checklist items confirmed: store connections, server notifications, purchase flow, access level checks, and privacy requirements. - **Gotcha:** Missing server notifications → configure App Store Server Notifications in **App settings → iOS SDK** and Google Play Real-Time Developer Notifications in **App settings → Android SDK**. ::: ## Plain text doc index files If you need to give your LLM broader context beyond individual pages, we host index files that list or combine all Adapty documentation: - [`llms.txt`](https://adapty.io/docs/llms.txt): Lists all pages with `.md` links. An [emerging standard](https://llmstxt.org/) for making websites accessible to LLMs. Note that for some AI agents (e.g., ChatGPT) you will need to download `llms.txt` and upload it to the chat as a file. - [`llms-full.txt`](https://adapty.io/docs/llms-full.txt): The entire Adapty documentation site combined into a single file. Very large — use only when you need the full picture. - Flutter-specific [`flutter-llms.txt`](https://adapty.io/docs/flutter-llms.txt) and [`flutter-llms-full.txt`](https://adapty.io/docs/flutter-llms-full.txt): Platform-specific subsets that save tokens compared to the full site. --- # File: flutter-get-pb-paywalls --- --- title: "Fetch Paywall Builder paywalls and their configuration in Flutter SDK" description: "Learn how to retrieve PB paywalls in Adapty for better subscription control in Flutter." --- After [you designed the visual part for your paywall](adapty-paywall-builder) with the new Paywall Builder in the Adapty Dashboard, you can display it in your mobile app. The first step in this process is to get the paywall associated with the placement and its view configuration as described below. :::warning The new Paywall Builder works with Flutter SDK version 3.3.0 or higher. For presenting paywalls in Adapty SDK v2 designed with the legacy Paywall Builder, see [Display paywalls designed with legacy Paywall Builder](flutter-legacy). ::: Please be aware that this topic refers to Paywall Builder-customized paywalls. If you are implementing your paywalls manually, please refer to the [Fetch paywalls and products for remote config paywalls in your mobile app](fetch-paywalls-and-products-flutter) topic. :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. :::
Before you start displaying paywalls in your mobile app (click to expand) 1. [Create your products](create-product) in the Adapty Dashboard. 2. [Create a paywall and incorporate the products into it](create-paywall) in the Adapty Dashboard. 3. [Create placements and incorporate your paywall into it](create-placement) in the Adapty Dashboard. 4. Install [Adapty SDK](sdk-installation-flutter) in your mobile app.
## Fetch paywall designed with Paywall Builder If you've [designed a paywall using the Paywall Builder](adapty-paywall-builder), you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. Nevertheless, you need to get its ID via the placement, its view configuration, and then present it in your mobile app. To ensure optimal performance, it's crucial to retrieve the paywall and its [view configuration](flutter-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) as early as possible, allowing sufficient time for images to download before presenting them to the user. To get a paywall, use the `getPaywall` method: ```dart showLineNumbers try { final paywall = await Adapty().getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en"); // the requested paywall } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Parameters: | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the [paywall localization](add-paywall-locale-in-adapty-paywall-builder). This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

See [Localizations and locale codes](flutter-localizations-and-locale-codes) for more information on locale codes and how we recommend using them.

| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

Adapty SDK stores paywalls locally in two layers: regularly updated cache described above and [fallback paywalls](fallback-paywalls). We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.

| | **loadTimeout** | default: 5 sec |

This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.

Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.

For Android: You can create `TimeInterval` with extension functions (like `5.seconds`, where `.seconds` is from `import com.adapty.utils.seconds`), or `TimeInterval.seconds(5)`. To set no limitation, use `TimeInterval.INFINITE`.

| Response parameters: | Parameter | Description | | :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html) object with a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch the view configuration of paywall designed using Paywall Builder :::important Make sure to enable the **Show on device** toggle in the paywall builder. If this option isn't turned on, the view configuration won't be available to retrieve. ::: After fetching the paywall, check if it includes a `ViewConfiguration`, which indicates that it was created using Paywall Builder. This will guide you on how to display the paywall. If the `ViewConfiguration` is present, treat it as a Paywall Builder paywall; if not, [handle it as a remote config paywall](present-remote-config-paywalls-flutter). ```dart showLineNumbers try { final view = await AdaptyUI().createPaywallView( paywall: paywall, ); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` Once you have the view, [present the paywall](flutter-present-paywalls). ## Get a paywall for a default audience to fetch it faster Typically, paywalls are fetched almost instantly, so you don’t need to worry about speeding up this process. However, in cases where you have numerous audiences and paywalls, and your users have a weak internet connection, fetching a paywall may take longer than you'd like. In such situations, you might want to display a default paywall to ensure a smooth user experience rather than showing no paywall at all. To address this, you can use the `getPaywallForDefaultAudience` method, which fetches the paywall of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the paywall by the `getPaywall` method, as detailed in the [Fetch Paywall Information](flutter-get-pb-paywalls#fetch-paywall-designed-with-paywall-builder) section above. :::warning Why we recommend using `getPaywall` The `getPaywallForDefaultAudience` method comes with a few significant drawbacks: - **Potential backward compatibility issues**: If you need to show different paywalls for different app versions (current and future), you may face challenges. You’ll either have to design paywalls that support the current (legacy) version or accept that users with the current (legacy) version might encounter issues with non-rendered paywalls. - **Loss of targeting**: All users will see the same paywall designed for the **All Users** audience, which means you lose personalized targeting (including based on countries, marketing attribution or your own custom attributes). If you're willing to accept these drawbacks to benefit from faster paywall fetching, use the `getPaywallForDefaultAudience` method as follows. Otherwise stick to `getPaywall` described [above](#fetch-paywall-designed-with-paywall-builder). ::: ```dart showLineNumbers try { final paywall = await Adapty().getPaywallForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID'); } on AdaptyError catch (adaptyError) { // handle error } catch (e) { // handle unknown error } ``` :::note The `getPaywallForDefaultAudience` method is available starting from Flutter SDK version 3.2.0. ::: | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the [Placement](placements). This is the value you specified when creating a placement in your Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.

| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

| ## Customize assets To customize images and videos in your paywall, implement the custom assets. Hero images and videos have predefined IDs: `hero_image` and `hero_video`. In a custom asset bundle, you target these elements by their IDs and customize their behavior. For other images and videos, you need to [set a custom ID](https://adapty.io/docs/custom-media) in the Adapty dashboard. For example, you can: - Show a different image or video to some users. - Show a local preview image while a remote main image is loading. - Show a preview image before running a video. :::important To use this feature, update the Adapty Flutter SDK to version 3.8.0 or higher. ::: Here’s an example of how you can provide custom asssets via a simple dictionary: ```dart final customAssets = { // Show a local image using a custom ID 'custom_image': AdaptyCustomAsset.localImageAsset( assetId: 'assets/images/image_name.png', ), // Show a local video with a preview image 'hero_video': AdaptyCustomAsset.localVideoAsset( assetId: 'assets/videos/custom_video.mp4', ), }; try { final view = await AdaptyUI().createPaywallView( paywall: paywall, customAssets: , preloadProducts: preloadProducts, ); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` :::note If an asset is not found, the paywall will fall back to its default appearance. ::: ## Set up developer-defined timers To use custom timers in your mobile app, create an object that follows the `AdaptyTimerResolver` protocol. This object defines how each custom timer should be rendered. If you prefer, you can use a `[String: Date]` dictionary directly, as it already conforms to this protocol. Here is an example: ```dart showLineNumbers try { final view = await AdaptyUI().createPaywallView( paywall: paywall, customTimers: { 'CUSTOM_TIMER_6H': DateTime.now().add(const Duration(seconds: 3600 * 6)), 'CUSTOM_TIMER_NY': DateTime(2025, 1, 1), // New Year 2025 }, ); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` In this example, `CUSTOM_TIMER_NY` and `CUSTOM_TIMER_6H` are the **Timer ID**s of developer-defined timers you set in the Adapty Dashboard. The `timerResolver` ensures your app dynamically updates each timer with the correct value. For example: - `CUSTOM_TIMER_NY`: The time remaining until the timer’s end, such as New Year’s Day. - `CUSTOM_TIMER_6H`: The time left in a 6-hour period that started when the user opened the paywall. --- # File: flutter-present-paywalls --- --- title: "Flutter - Present new Paywall Builder paywalls" description: "Present paywalls in Flutter apps using Adapty’s monetization features." --- If you've customized a paywall using the Paywall Builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. :::warning This guide is for **new Paywall Builder paywalls** only which require SDK v3.2.0 or later. The process for presenting paywalls differs for paywalls designed with different versions of Paywall Builde and remote config paywalls. - For presenting **Legacy Paywall Builder paywalls**, check out [Flutter - Present legacy Paywall Builder paywalls](flutter-present-paywalls-legacy). - For presenting **Remote config paywalls**, see [Render paywall designed by remote config](present-remote-config-paywalls). ::: Adapty Flutter SDK provides two ways to present paywalls: - **Standalone screen** - **Embedded widget** ## Present as standalone screen To display a paywall as a standalone screen, use the `view.present()` method on the `view` created by the [`createPaywallView`](flutter-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) method. Each `view` can only be used once. If you need to display the paywall again, call `createPaywallView` one more time to create a new `view` instance. :::warning Reusing the same `view` without recreating it may result in an `AdaptyUIError.viewAlreadyPresented` error. ::: ```dart showLineNumbers title="Flutter" try { await view.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ### Dismiss the paywall When you need to programmatically close the paywall, use the `dismiss()` method: ```dart showLineNumbers title="Flutter" try { await view.dismiss(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ### Show dialog Use this method instead of native alert dialogs when a paywall view is presented on Android. On Android, regular alerts appear behind the paywall view, which makes them invisible to users. This method ensures proper dialog presentation above the paywall on all platforms. ```dart showLineNumbers title="Flutter" try { final action = await view.showDialog( title: 'Close paywall?', content: 'You will lose access to exclusive offers.', primaryActionTitle: 'Stay', secondaryActionTitle: 'Close', ); if (action == AdaptyUIDialogActionType.secondary) { // User confirmed - close the paywall await view.dismiss(); } // If primary - do nothing, user stays } catch (e) { // handle error } ``` ### Configure iOS presentation style Configure how the paywall is presented on iOS by passing the `iosPresentationStyle` parameter to the `present()` method. The parameter accepts `AdaptyUIIOSPresentationStyle.fullScreen` (default) or `AdaptyUIIOSPresentationStyle.pageSheet` values. ```dart showLineNumbers try { await view.present(iosPresentationStyle: AdaptyUIIOSPresentationStyle.pageSheet); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ## Embed in widget hierarchy To embed a paywall within your existing widget tree, use the `AdaptyUIPaywallPlatformView` widget directly in your Flutter widget hierarchy. ```dart showLineNumbers title="Flutter" AdaptyUIPaywallPlatformView( paywall: paywall, // The paywall object you fetched onDidAppear: (view) { }, onDidDisappear: (view) { }, onDidPerformAction: (view, action) { }, onDidSelectProduct: (view, productId) { }, onDidStartPurchase: (view, product) { }, onDidFinishPurchase: (view, product, purchaseResult) { }, onDidFailPurchase: (view, product, error) { }, onDidStartRestore: (view) { }, onDidFinishRestore: (view, profile) { }, onDidFailRestore: (view, error) { }, onDidFailRendering: (view, error) { }, onDidFailLoadingProducts: (view, error) { }, onDidFinishWebPaymentNavigation: (view, product, error) { }, ) ``` :::note For Android platform view to work, ensure your `MainActivity` extends `FlutterFragmentActivity`: ```kotlin showLineNumbers title="Kotlin" class MainActivity : FlutterFragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } } ``` ::: --- # File: flutter-handle-paywall-actions --- --- title: "Respond to button actions in Flutter SDK" description: "Handle paywall button actions in Flutter using Adapty for better app monetization." --- If you are building paywalls using the Adapty paywall builder, it's crucial to set up buttons properly: 1. Add a [button in the paywall builder](paywall-buttons.md) and assign it either a pre-existing action or create a custom action ID. 2. Write code in your app to handle each action you've assigned. This guide shows how to handle custom and pre-existing actions in your code. :::warning **Only purchases and restorations are handled automatically.** All the other button actions, such as closing paywalls or opening links, require implementing proper responses in the app code. ::: ## Close paywalls To add a button that will close your paywall: 1. In the paywall builder, add a button and assign it the **Close** action. 2. In your app code, implement a handler for the `CloseAction` and `AndroidSystemBackAction` actions. :::info In the Flutter SDK, the `CloseAction` and `AndroidSystemBackAction` actions trigger closing the paywall by default. However, you can override this behavior in your code if needed. For example, closing one paywall might trigger opening another. ::: ```dart void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) { switch (action) { case const CloseAction(): case const AndroidSystemBackAction(): view.dismiss(); break; default: break; } } ``` ## Open URLs from paywalls :::tip If you want to add a group of links (e.g., terms of use and purchase restoration), add a **Link** element in the paywall builder and handle it the same way as buttons with the **Open URL** action. ::: To add a button that opens a link from your paywall (e.g., **Terms of use** or **Privacy policy**): 1. In the paywall builder, add a button, assign it the **Open URL** action, and enter the URL you want to open. 2. In your app code, implement a handler for the `openUrl` action that opens the received URL in a browser. ```dart // You have to install url_launcher plugin in order to handle urls: // https://pub.dev/packages/url_launcher void paywallViewDidPerformAction(AdaptyUIView view, AdaptyUIAction action) { switch (action) { case OpenUrlAction(url: final url): final Uri uri = Uri.parse(url); launchUrl(uri, mode: LaunchMode.inAppBrowserView); break; default: break; } } ``` ## Log into the app To add a button that logs users into your app: 1. In the paywall builder, add a button and assign it the **Login** action. 2. In your app code, implement a handler for the `login` action that identifies your user. ```dart void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) { switch (action) { case CustomAction(action: 'login'): // Handle login action Navigator.of(context).push(MaterialPageRoute(builder: (context) => LoginScreen())); break; default: break; } } ``` ## Handle custom actions To add a button that handles any other actions: 1. In the paywall builder, add a button, assign it the **Custom** action, and assign it an ID. 2. In your app code, implement a handler for the action ID you've created. For example, if you have another set of subscription offers or one-time purchases, you can add a button that will display another paywall: ```dart void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) { switch (action) { case CustomAction(action: 'openNewPaywall'): // Display another paywall break; default: break; } } ``` --- # File: flutter-handling-events --- --- title: "Flutter - Handle paywall events" description: "Discover how to handle subscription-related events in Flutter using Adapty to track user interactions effectively." --- :::important This guide covers event handling for purchases, restorations, product selection, and paywall rendering. You must also implement button handling (closing paywall, opening links, etc.). See our [guide on handling button actions](flutter-handle-paywall-actions.md) for details. ::: Paywalls configured with the [Paywall Builder](adapty-paywall-builder-legacy) don't need extra code to make and restore purchases. However, they generate some events that your app can respond to. Those events include button presses (close buttons, URLs, product selections, and so on) as well as notifications on purchase-related actions taken on the paywall. Learn how to respond to these events below. :::warning This guide is for **new Paywall Builder paywalls** only which require Adapty SDK v3.0 or later. For presenting paywalls in Adapty SDK v2 designed with legacy Paywall Builder, see [Flutter - Handle paywall events designed with legacy Paywall Builder](flutter-handling-events-legacy). ::: To control or monitor processes occurring on the paywall screen within your mobile app, implement the `AdaptyUIPaywallsEventsObserver` methods and set the observer before presenting any screen: ```javascript showLineNumbers title="Flutter" AdaptyUI().setPaywallsEventsObserver(this); ``` :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ### User-generated events #### Paywall appeared This method is invoked when the paywall view is presented on the screen. :::note On iOS, also invoked when a user taps the [web paywall button](web-paywall#step-2a-add-a-web-purchase-button) inside a paywall, and a web paywall opens in an in-app browser. ::: ```javascript showLineNumbers title="Flutter" void paywallViewDidAppear(AdaptyUIPaywallView view) { } ``` #### Paywall disappeared This method is invoked when the paywall view is dismissed from the screen. :::note On iOS, also invoked when a [web paywall](web-paywall#step-2a-add-a-web-purchase-button) opened from a paywall in an in-app browser disappears from the screen. ::: ```javascript showLineNumbers title="Flutter" void paywallViewDidDisappear(AdaptyUIPaywallView view) { } ``` #### Product selection If a product is selected for purchase (by a user or by the system), this method will be invoked: ```javascript showLineNumbers title="Flutter" void paywallViewDidSelectProduct(AdaptyUIPaywallView view, String productId) { } ```
Event example (Click to expand) ```javascript { "productId": "premium_monthly" } ```
#### Started purchase If a user initiates the purchase process, this method will be invoked: ```javascript showLineNumbers title="Flutter" void paywallViewDidStartPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product) { } ```
Event example (Click to expand) ```javascript { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } ```
#### Finished purchase This method is invoked when a purchase succeeds, the user cancels their purchase, or the purchase appears to be pending: ```javascript showLineNumbers title="Flutter" void paywallViewDidFinishPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyPurchaseResult purchaseResult) { switch (purchaseResult) { case AdaptyPurchaseResultSuccess(profile: final profile): // successful purchase break; case AdaptyPurchaseResultPending(): // purchase is pending break; case AdaptyPurchaseResultUserCancelled(): // user cancelled the purchase break; default: break; } } ```
Event examples (Click to expand) ```javascript // Successful purchase { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "purchaseResult": { "type": "AdaptyPurchaseResultSuccess", "profile": { "accessLevels": { "premium": { "id": "premium", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } } } } } // Pending purchase { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "purchaseResult": { "type": "AdaptyPurchaseResultPending" } } // User cancelled purchase { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "purchaseResult": { "type": "AdaptyPurchaseResultUserCancelled" } } ```
We recommend dismissing the screen in that case. Refer to [Respond to button actions](flutter-handle-paywall-actions.md) for details on dismissing a paywall screen. #### Finished web payment navigation This method is invoked after an attempt to open a [web paywall](web-paywall.md) for a specific product. This includes both successful and failed navigation attempts: ```javascript showLineNumbers title="Flutter" void paywallViewDidFinishWebPaymentNavigation(AdaptyUIPaywallView view, AdaptyPaywallProduct? product, AdaptyError? error) { } ``` **Parameters:** | Parameter | Description | |:------------|:---------------------------------------------------------------------------------------------------| | **product** | An `AdaptyPaywallProduct` for which the web paywall was opened. Can be `null`. | | **error** | An `AdaptyError` object if the web paywall navigation failed; `null` if navigation was successful. |
Event examples (Click to expand) ```javascript // Successful navigation { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "error": null } // Failed navigation { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "error": { "code": "web_navigation_failed", "message": "Failed to open web paywall", "details": { "underlyingError": "Browser unavailable" } } } ```
#### Failed purchase This method is invoked when a purchase fails (for example, due to payment issues or network errors). It does **not** fire for user-initiated cancellations or pending transactions—those are handled by `paywallViewDidFinishPurchase`: ```javascript showLineNumbers title="Flutter" void paywallViewDidFailPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyError error) { } ```
Event example (Click to expand) ```javascript { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "error": { "code": "purchase_failed", "message": "Purchase failed due to insufficient funds", "details": { "underlyingError": "Insufficient funds in account" } } } ```
#### Started restore If a user initiates the restore process, this method will be invoked: ```javascript showLineNumbers title="Flutter" void paywallViewDidStartRestore(AdaptyUIPaywallView view) { } ``` #### Successful restore If restoring a purchase succeeds, this method will be invoked: ```javascript showLineNumbers title="Flutter" void paywallViewDidFinishRestore(AdaptyUIPaywallView view, AdaptyProfile profile) { } ```
Event example (Click to expand) ```javascript { "profile": { "accessLevels": { "premium": { "id": "premium", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } }, "subscriptions": [ { "vendorProductId": "premium_monthly", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } ] } } ```
We recommend dismissing the screen if the user has the required `accessLevel`. Refer to the [Subscription status](flutter-listen-subscription-changes.md) topic to learn how to check it and to [Respond to button actions](flutter-handle-paywall-actions.md) topic to learn how to dismiss a paywall screen. #### Failed restore If restoring a purchase fails, this method will be invoked: ```javascript showLineNumbers title="Flutter" void paywallViewDidFailRestore(AdaptyUIPaywallView view, AdaptyError error) { } ```
Event example (Click to expand) ```javascript { "error": { "code": "restore_failed", "message": "Purchase restoration failed", "details": { "underlyingError": "No previous purchases found" } } } ```
### Data fetching and rendering #### Product loading errors If you don't pass the product array during the initialization, AdaptyUI will retrieve the necessary objects from the server by itself. If this operation fails, AdaptyUI will report the error by invoking this method: ```javascript showLineNumbers title="Flutter" void paywallViewDidFailLoadingProducts(AdaptyUIPaywallView view, AdaptyError error) { } ```
Event example (Click to expand) ```javascript { "error": { "code": "products_loading_failed", "message": "Failed to load products from the server", "details": { "underlyingError": "Network timeout" } } } ```
#### Rendering errors If an error occurs during the interface rendering, it will be reported by calling this method. By default (since v3.15.2), the paywall is automatically dismissed when a rendering error occurs, but you can override this behavior if needed. ```javascript showLineNumbers title="Flutter" void paywallViewDidFailRendering(AdaptyUIPaywallView view, AdaptyError error) { // Default behavior: view.dismiss() // Override with custom logic if needed, for example: // - Log the error // - Show an error message to the user } ```
Event example (Click to expand) ```javascript { "error": { "code": "rendering_failed", "message": "Failed to render paywall interface", "details": { "underlyingError": "Invalid paywall configuration" } } } ```
In a normal situation, such errors should not occur, so if you come across one, please let us know. --- # File: flutter-use-fallback-paywalls --- --- title: "Flutter - Use fallback paywalls" description: "Handle cases when users are offline or Adapty servers aren't available" --- :::warning Fallback paywalls are supported by Flutter SDK v2.11 and later. ::: To maintain a fluid user experience, it is important to set up [fallbacks](/fallback-paywalls) for your [paywalls](paywalls) and [onboardings](onboardings). This precaution extends the application's capabilities in case of partial or complete loss of internet connection. * **If the application cannot access Adapty servers:** It will be able to display a fallback paywall, and access the local onboarding configuration. * **If the application cannot access the internet:** It will be able to display a fallback paywall. Onboardings include remote content and require an internet connection to function. :::important Before you follow the steps in this guide, [download](/local-fallback-paywalls) the fallback configuration files from Adapty. ::: ## Configuration 1. Add the fallback configuration files to the app’s `assets` directory at the project root. 2. Call the `.setFallback` method **before** you fetch the target paywall or onboarding. ```javascript showLineNumbers title="javascript" final assetId = Platform.isIOS ? 'assets/ios_fallback.json' : 'assets/android_fallback.json'; try { await Adapty.setFallback(assetId); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Parameters: | Parameter | Description | | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **assetId** | Path to the fallback configuration file. | :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: --- # File: flutter-web-paywall --- --- title: "Implement web paywalls in Flutter SDK" description: "Set up a web paywall to get paid without the App Store fees and audits." --- :::important Before you begin, make sure you have [configured your web paywall in the dashboard](web-paywall.md) and installed Adapty SDK version 3.6.1 or later. ::: If you are working with a paywall you developed yourself, you need to handle web paywalls using the SDK method. The `.openWebPaywall` method: 1. Generates a unique URL allowing Adapty to link a specific paywall shown to a particular user to the web page they are redirected to. 2. Tracks when your users return to the app and then requests `.getProfile` at short intervals to determine whether the profile access rights have been updated. This way, if the payment has been successful and access rights have been updated, the subscription activates in the app almost immediately. ```dart showLineNumbers title="Flutter" try { await Adapty().openWebPaywall(product: ); // The web paywall will be opened } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle other errors } ``` :::note There are two versions of the `openWebPaywall` method: 1. `openWebPaywall(product)` that generates URLs by paywall and adds the product data to URLs as well. 2. `openWebPaywall(paywall)` that generates URLs by paywall without adding the product data to URLs. Use it when your products in the Adapty paywall differ from those in the web paywall. ::: #### Handle errors | Error | Description | Recommended action | |-----------------------------------------|--------------------------------------------------------|---------------------------------------------------------------------------| | AdaptyError.paywallWithoutPurchaseUrl | The paywall doesn't have a web purchase URL configured | Check if the paywall has been properly configured in the Adapty Dashboard | | AdaptyError.productWithoutPurchaseUrl | The product doesn't have a web purchase URL | Verify the product configuration in the Adapty Dashboard | | AdaptyError.failedOpeningWebPaywallUrl | Failed to open the URL in the browser | Check device settings or provide an alternative purchase method | | AdaptyError.failedDecodingWebPaywallUrl | Failed to properly encode parameters in the URL | Verify URL parameters are valid and properly formatted | ## Open web paywalls in an in-app browser :::important Opening web paywalls in an in-app browser is supported starting from Adapty SDK v. 3.15. ::: By default, web paywalls open in the external browser. To provide a seamless user experience, you can open web paywalls in an in-app browser. This displays the web purchase page within your application, allowing users to complete transactions without switching apps. To enable this, set the `in` parameter to `.inAppBrowser`: ```dart showLineNumbers try { await Adapty().openWebPaywall( product: , openIn: AdaptyWebPresentation.inAppBrowser, ); // The web paywall will be opened in the in-app browser } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle other errors } ``` --- # File: flutter-troubleshoot-paywall-builder --- --- title: "Troubleshoot Paywall Builder in Flutter SDK" description: "Troubleshoot Paywall Builder in Flutter SDK" --- This guide helps you resolve common issues when using paywalls designed in the Adapty Paywall Builder in the Flutter SDK. ## Getting a paywall configuration fails **Issue**: The `createPaywallView` method fails to retrieve paywall configuration. **Reason**: The paywall is not enabled for device display in the Paywall Builder. **Solution**: Enable the **Show on device** toggle in the Paywall Builder. ## The paywall view number is too big **Issue**: The paywall view count is showing double the expected number. **Reason**: You may be calling `logShowPaywall` in your code, which duplicates the view count if you're using the Paywall builder. For paywalls designed with the Paywall Builder, analytics are tracked automatically, so you don't need to use this method. **Solution**: Ensure you are not calling `logShowPaywall` in your code if you're using the Paywall builder. ## Other issues **Issue**: You're experiencing other Paywall Builder-related problems not covered above. **Solution**: Migrate the SDK to the latest version using the [migration guides](flutter-sdk-migration-guides) if needed. Many issues are resolved in newer SDK versions. --- # File: flutter-quickstart-manual --- --- title: "Enable purchases in your custom paywall in Flutter SDK" description: "Integrate Adapty SDK into your custom Flutter paywalls to enable in-app purchases." --- This guide describes how to integrate Adapty into your custom paywalls. Keep full control over paywall implementation, while the Adapty SDK fetches products, handles new purchases, and restores previous ones. :::important **This guide is for developers who are implementing custom paywalls.** If you want the easiest way to enable purchases, use the [Adapty Paywall Builder](flutter-quickstart-paywalls.md). With Paywall Builder, you create paywalls in a no-code visual editor, Adapty handles all purchase logic automatically, and you can test different designs without republishing your app. ::: ## Before you start ### Set up products To enable in-app purchases, you need to understand three key concepts: - [**Products**](product.md) – anything users can buy (subscriptions, consumables, lifetime access) - [**Paywalls**](paywalls.md) – configurations that define which products to offer. In Adapty, paywalls are the only way to retrieve products, but this design lets you modify products, prices, and offers without touching your app code. - [**Placements**](placements.md) – where and when you show paywalls in your app (like `main`, `onboarding`, `settings`). You set up paywalls for placements in the dashboard, then request them by placement ID in your code. This makes it easy to run A/B tests and show different paywalls to different users. Make sure you understand these concepts even if you work with your custom paywall. Basically, they are just your way to manage the products you sell in your app. To implement your custom paywall, you will need to create a **paywall** and add it to a **placement**. This setup allows you to retrieve your products. To understand what you need to do in the dashboard, follow the quickstart guide [here](quickstart.md). ### Manage users You can work either with or without backend authentication on your side. However, the Adapty SDK handles anonymous and identified users differently. Read the [identification quickstart guide](flutter-quickstart-identify.md) to understand the specifics and ensure you are working with users properly. ## Step 1. Get products To retrieve products for your custom paywall, you need to: 1. Get the `paywall` object by passing [placement](placements.md) ID to the `getPaywall` method. 2. Get the products array for this paywall using the `getPaywallProducts` method. ```dart showLineNumbers Future loadPaywall() async { try { final paywall = await Adapty().getPaywall(placementId: 'YOUR_PLACEMENT_ID'); final products = await Adapty().getPaywallProducts(paywall: paywall); // Use products to build your custom paywall UI } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // Handle the error } } ``` ## Step 2. Accept purchases When a user taps on a product in your custom paywall, call the `makePurchase` method with the selected product. This will handle the purchase flow and return the updated profile. ```dart showLineNumbers Future purchaseProduct(AdaptyPaywallProduct product) async { try { final purchaseResult = await Adapty().makePurchase(product: product); switch (purchaseResult) { case AdaptyPurchaseResultSuccess(profile: final profile): // Purchase successful, profile updated break; case AdaptyPurchaseResultUserCancelled(): // User canceled the purchase break; case AdaptyPurchaseResultPending(): // Purchase is pending (e.g., user will pay offline with cash) break; } } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // Handle the error } } ``` ## Step 3. Restore purchases App stores require all apps with subscriptions to provide a way users can restore their purchases. Call the `restorePurchases` method when the user taps the restore button. This will sync their purchase history with Adapty and return the updated profile. ```dart showLineNumbers Future restorePurchases() async { try { final profile = await Adapty().restorePurchases(); // Restore successful, profile updated } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // Handle the error } } ``` ## Next steps Your paywall is ready to be displayed in the app. Test your purchases in the [App Store sandbox](test-purchases-in-sandbox) or in [Google Play Store](testing-on-android) to make sure you can complete a test purchase from the paywall. To see how this works in a production-ready implementation, check out the [PurchasesObserver](https://github.com/adaptyteam/AdaptySDK-Flutter/blob/master/example/lib/purchase_observer.dart) in our example app, which demonstrates purchase handling with proper error handling, UI observers, and comprehensive SDK integration. Next, [check whether users have completed their purchase](flutter-check-subscription-status.md) to determine whether to display the paywall or grant access to paid features. --- # File: fetch-paywalls-and-products-flutter --- --- title: "Fetch paywalls and products for remote config paywalls in Flutter SDK" description: "Fetch paywalls and products in Adapty Flutter SDK to enhance user monetization." --- Before showcasing remote config and custom paywalls, you need to fetch the information about them. Please be aware that this topic refers to remote config and custom paywalls. For guidance on fetching paywalls for Paywall Builder-customized paywalls, please consult [Fetch Paywall Builder paywalls and their configuration](flutter-get-pb-paywalls). :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. :::
Before you start fetching paywalls and products in your mobile app (click to expand) 1. [Create your products](create-product) in the Adapty Dashboard. 2. [Create a paywall and incorporate the products into your paywall](create-paywall) in the Adapty Dashboard. 3. [Create placements and incorporate your paywall into the placement](create-placement) in the Adapty Dashboard. 4. [Install Adapty SDK](sdk-installation-flutter) in your mobile app.
## Fetch paywall information In Adapty, a [product](product) serves as a combination of products from both the App Store and Google Play. These cross-platform products are integrated into paywalls, enabling you to showcase them within specific mobile app placements. To display the products, you need to obtain a [Paywall](paywalls) from one of your [placements](placements) with `getPaywall` method. :::important **Don't hardcode product IDs.** The only ID you should hardcode is the placement ID. Paywalls are configured remotely, so the number of products and available offers can change at any time. Your app must handle these changes dynamically—if a paywall returns two products today and three tomorrow, display all of them without code changes. ::: ```dart showLineNumbers try { final paywall = await Adapty().getPaywall(id: "YOUR_PLACEMENT_ID", locale: "en"); // the requested paywall } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the [Placement](placements). This is the value you specified when creating a placement in your Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

See [Localizations and locale codes](flutter-localizations-and-locale-codes) for more information on locale codes and how we recommend using them.

| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

Adapty SDK stores paywalls in two layers: regularly updated cache described above and [fallback paywalls](flutter-use-fallback-paywalls) . We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.

| | **loadTimeout** | default: 5 sec |

This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.

Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.

| Don't hardcode product IDs! Since paywalls are configured remotely, the available products, the number of products, and special offers (such as free trials) can change over time. Make sure your code handles these scenarios. For example, if you initially retrieve 2 products, your app should display those 2 products. However, if you later retrieve 3 products, your app should display all 3 without requiring any code changes. The only thing you have to hardcode is placement ID. Response parameters: | Parameter | Description | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html) object with: a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch products Once you have the paywall, you can query the product array that corresponds to it: ```dart showLineNumbers try { final products = await Adapty().getPaywallProducts(paywall: paywall); // the requested products array } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Response parameters: | Parameter | Description | | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Products | List of [`AdaptyPaywallProduct`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywallProduct-class.html) objects with: product identifier, product name, price, currency, subscription length, and several other properties. | When implementing your own paywall design, you will likely need access to these properties from the [`AdaptyPaywallProduct`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywallProduct-class.html) object. Illustrated below are the most commonly used properties, but refer to the linked document for full details on all available properties. | Property | Description | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Title** | To display the title of the product, use `product.localizedTitle`. Note that the localization is based on the users' selected store country rather than the locale of the device itself. | | **Price** | To display a localized version of the price, use `product.price.localizedString`. This localization is based on the locale info of the device. You can also access the price as a number using `product.price.amount`. The value will be provided in the local currency. To get the associated currency symbol, use `product.price.currencySymbol`. | | **Subscription Period** | To display the period (e.g. week, month, year, etc.), use `product.subscription?.localizedPeriod`. This localization is based on the locale of the device. To fetch the subscription period programmatically, use `product.subscription?.period`. From there you can access the `unit` enum to get the length (i.e. day, week, month, year, or unknown). The `numberOfUnits` value will get you the number of period units. For example, for a quarterly subscription, you'd see `AdaptyPeriodUnit.month` in the unit property, and `3` in the numberOfUnits property. | | **Introductory Offer** | To display a badge or other indicator that a subscription contains an introductory offer, check out the `product.subscription?.offer?.phases` property. This is a list that can contain up to two discount phases: the free trial phase and the introductory price phase. Within each phase object are the following helpful properties:
• `paymentMode`: an enum with values `AdaptyPaymentMode.freeTrial`, `AdaptyPaymentMode.payAsYouGo`, `AdaptyPaymentMode.payUpFront`, and `AdaptyPaymentMode.unknown`. Free trials will be the `AdaptyPaymentMode.freeTrial` type.
• `price`: The discounted price as a number. For free trials, look for `0` here.
• `localizedNumberOfPeriods`: a string localized using the device's locale describing the length of the offer. For example, a three day trial offer shows `3 days` in this field.
• `subscriptionPeriod`: Alternatively, you can get the individual details of the offer period with this property. It works in the same manner for offers as the previous section describes.
• `localizedSubscriptionPeriod`: A formatted subscription period of the discount for the user's locale. | ## Speed up paywall fetching with default audience paywall Typically, paywalls are fetched almost instantly, so you don't need to worry about speeding up this process. However, in cases where you have numerous audiences and paywalls, and your users have a weak internet connection, fetching a paywall may take longer than you'd like. In such situations, you might want to display a default paywall to ensure a smooth user experience rather than showing no paywall at all. To address this, you can use the `getPaywallForDefaultAudience` method, which fetches the paywall of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the paywall by the `getPaywall` method, as detailed in the [Fetch Paywall Information](fetch-paywalls-and-products-flutter#fetch-paywall-information) section above. :::warning Why we recommend using `getPaywall` The `getPaywallForDefaultAudience` method comes with a few significant drawbacks: - **Potential backward compatibility issues**: If you need to show different paywalls for different app versions (current and future), you may face challenges. You'll either have to design paywalls that support the current (legacy) version or accept that users with the current (legacy) version might encounter issues with non-rendered paywalls. - **Loss of targeting**: All users will see the same paywall designed for the **All Users** audience, which means you lose personalized targeting (including based on countries, marketing attribution or your own custom attributes). If you're willing to accept these drawbacks to benefit from faster paywall fetching, use the `getPaywallForDefaultAudience` method as follows. Otherwise, stick to the `getPaywall` described [above](fetch-paywalls-and-products-flutter#fetch-paywall-information). ::: :::note The `getPaywallForDefaultAudience` method is not yet supported in Flutter SDK, but support will be added soon. ::: | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the [Placement](placements). This is the value you specified when creating a placement in your Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

See [Localizations and locale codes](flutter-localizations-and-locale-codes) for more information on locale codes and how we recommend using them.

| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

| --- # File: present-remote-config-paywalls-flutter --- --- title: "Render paywall designed by remote config in Flutter SDK" description: "Discover how to present remote config paywalls in Adapty Flutter SDK to personalize user experience." --- If you've customized a paywall using remote config, you'll need to implement rendering in your mobile app's code to display it to users. Since remote config offers flexibility tailored to your needs, you're in control of what's included and how your paywall view appears. We provide a method for fetching the remote configuration, giving you the autonomy to showcase your custom paywall configured via remote config. ## Get paywall remote config and present it To get a remote config of a paywall, access the `remoteConfig` property and extract the needed values. ```dart showLineNumbers try { final paywall = await Adapty().getPaywall(id: "YOUR_PLACEMENT_ID"); final String? headerText = paywall.remoteConfig?['header_text']; } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` At this point, once you've received all the necessary values, it's time to render and assemble them into a visually appealing page. Ensure that the design accommodates various mobile phone screens and orientations, providing a seamless and user-friendly experience across different devices. :::warning Make sure to [record the paywall view event](present-remote-config-paywalls-flutter#track-paywall-view-events) as described below, allowing Adapty analytics to capture information for funnels and A/B tests. ::: After you've done with displaying the paywall, continue with setting up a purchase flow. When the user makes a purchase, simply call `.makePurchase()` with the product from your paywall. For details on the`.makePurchase()` method, read [Making purchases](flutter-making-purchases). We recommend [creating a backup paywall called a fallback paywall](flutter-use-fallback-paywalls). This backup will display to the user when there's no internet connection or cache available, ensuring a smooth experience even in these situations. ## Track paywall view events Adapty assists you in measuring the performance of your paywalls. While we gather data on purchases automatically, logging paywall views needs your input because only you know when a customer sees a paywall. To log a paywall view event, simply call `.logShowPaywall(paywall)`, and it will be reflected in your paywall metrics in funnels and A/B tests. :::important Calling `.logShowPaywall(paywall)` is not needed if you are displaying paywalls created in the [paywall builder](adapty-paywall-builder.md). ::: ```dart showLineNumbers try { final result = await Adapty().logShowPaywall(paywall: paywall); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Request parameters: | Parameter | Presence | Description | | :---------- | :------- |:----------------------------------------------------------------------| | **paywall** | required | An [`AdaptyPaywall`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html) object. | --- # File: flutter-making-purchases --- --- title: "Make purchases in mobile app in Flutter SDK" description: "Guide on handling in-app purchases and subscriptions using Adapty." --- Displaying paywalls within your mobile app is an essential step in offering users access to premium content or services. However, simply presenting these paywalls is enough to support purchases only if you use [Paywall Builder](adapty-paywall-builder) to customize your paywalls. If you don't use the Paywall Builder, you must use a separate method called `.makePurchase()` to complete a purchase and unlock the desired content. This method serves as the gateway for users to engage with the paywalls and proceed with their desired transactions. If your paywall has an active promotional offer for the product a user is trying to buy, Adapty will automatically apply it at the time of purchase. :::warning Keep in mind that the introductory offer will be applied automatically only if you use the paywalls set up using the Paywall Builder. In other cases, you'll need to [verify the user's eligibility for an introductory offer on iOS](fetch-paywalls-and-products-flutter#check-intro-offer-eligibility-on-ios). Skipping this step may result in your app being rejected during release. Moreover, it could lead to charging the full price to users who are eligible for an introductory offer. ::: Make sure you've [done the initial configuration](quickstart) without skipping a single step. Without it, we can't validate purchases. ## Make purchase :::note **Using [Paywall Builder](adapty-paywall-builder)?** Purchases are processed automatically—you can skip this step. **Looking for step-by-step guidance?** Check out the [quickstart guide](flutter-implement-paywalls-manually) for end-to-end implementation instructions with full context. ::: ```dart showLineNumbers try { final purchaseResult = await Adapty().makePurchase(product: product); switch (purchaseResult) { case AdaptyPurchaseResultSuccess(profile: final profile): if (profile.accessLevels['premium']?.isActive ?? false) { // Grant access to the paid features } break; case AdaptyPurchaseResultPending(): break; case AdaptyPurchaseResultUserCancelled(): break; default: break; } } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // Handle the error } ``` Request parameters: | Parameter | Presence | Description | | :---------- | :------- | :-------------------------------------------------------------------------------------------------- | | **Product** | required | An [`AdaptyPaywallProduct`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywallProduct-class.html) object retrieved from the paywall. | Response parameters: | Parameter | Description | |---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Profile** |

If the request has been successful, the response contains this object. An [AdaptyProfile](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyProfile-class.html) object provides comprehensive information about a user's access levels, subscriptions, and non-subscription purchases within the app.

Check the access level status to ascertain whether the user has the required access to the app.

| :::warning **Note:** if you're still on Apple's StoreKit version lower than v2.0 and Adapty SDK version lowers than v.2.9.0, you need to provide [Apple App Store shared secret](app-store-connection-configuration#step-4-enter-app-store-shared-secret) instead. This method is currently deprecated by Apple. ::: ## Change subscription when making a purchase When a user opts for a new subscription instead of renewing the current one, the way it works depends on the app store: - For the App Store, the subscription is automatically updated within the subscription group. If a user purchases a subscription from one group while already having a subscription from another, both subscriptions will be active at the same time. - For Google Play, the subscription isn't automatically updated. You'll need to manage the switch in your mobile app code as described below. To replace the subscription with another one in Android, call `.makePurchase()` method with the additional parameter: ```dart showLineNumbers try { final result = await adapty.makePurchase( product: product, subscriptionUpdateParams: subscriptionUpdateParams, ); // successful cross-grade } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // Handle the error } ``` Additional request parameter: | Parameter | Presence | Description | | :--------------------------- | :------- |:--------------------------------------------------------------------------------------------------------| | **subscriptionUpdateParams** | required | an [`AdaptyAndroidSubscriptionUpdateParameters`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyAndroidSubscriptionUpdateParameters-class.html) object. | You can read more about subscriptions and replacement modes in the Google Developer documentation: - [About replacement modes](https://developer.android.com/google/play/billing/subscriptions#replacement-modes) - [Recommendations from Google for replacement modes](https://developer.android.com/google/play/billing/subscriptions#replacement-recommendations) - Replacement mode [`CHARGE_PRORATED_PRICE`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#CHARGE_PRORATED_PRICE()). Note: this method is available only for subscription upgrades. Downgrades are not supported. - Replacement mode [`DEFERRED`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#DEFERRED()). Note: A real subscription change will occur only when the current subscription billing period ends. ## Redeem Offer Code in iOS Since iOS 14.0, your users can redeem Offer Codes. Code redemption means using a special code, like a promotional or gift card code, to get free access to content or features in an app or on the App Store. To enable users to redeem offer codes, you can display the offer code redemption sheet by using the appropriate SDK method: ```dart showLineNumbers try { await Adapty().presentCodeRedemptionSheet(); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` :::danger Based on our observations, the Offer Code Redemption sheet in some apps may not work reliably. We recommend redirecting the user directly to the App Store. In order to do this, you need to open the url of the following format: `https://apps.apple.com/redeem?ctx=offercodes&id={apple_app_id}&code={code}` ::: ### Manage prepaid plans (Android) If your app users can purchase [prepaid plans](https://developer.android.com/google/play/billing/subscriptions#prepaid-plans) (e.g., buy a non-renewable subscription for several months), you can enable [pending transactions](https://developer.android.com/google/play/billing/subscriptions#pending) for prepaid plans. ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withGoogleEnablePendingPrepaidPlans(true), ); ``` --- # File: flutter-restore-purchase --- --- title: "Restore purchases in mobile app in Flutter SDK" description: "Learn how to restore purchases in Adapty to ensure seamless user experience." --- Restoring Purchases in both iOS and Android is a feature that allows users to regain access to previously purchased content, such as subscriptions or in-app purchases, without being charged again. This feature is especially useful for users who may have uninstalled and reinstalled the app or switched to a new device and want to access their previously purchased content without paying again. :::note In paywalls built with [Paywall Builder](adapty-paywall-builder), purchases are restored automatically without additional code from you. If that's your case — you can skip this step. ::: To restore a purchase if you do not use the [Paywall Builder](adapty-paywall-builder) to customize the paywall, call `.restorePurchases()` method: ```javascript showLineNumbers try { final profile = await Adapty().restorePurchases(); if (profile?.accessLevels['YOUR_ACCESS_LEVEL']?.isActive ?? false) { // successful access restore } } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Response parameters: | Parameter | Description | |---------|-----------| | **Profile** |

An [`AdaptyProfile`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyProfile-class.html) object. This model contains info about access levels, subscriptions, and non-subscription purchases.

Сheck the **access level status** to determine whether the user has access to the app.

| :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: --- # File: implement-observer-mode-flutter --- --- title: "Implement Observer mode in Flutter SDK" description: "Implement observer mode in Adapty to track user subscription events in Flutter SDK." --- If you already have your own purchase infrastructure and aren't ready to fully switch to Adapty, you can explore [Observer mode](observer-vs-full-mode). In its basic form, Observer Mode offers advanced analytics and seamless integration with attribution and analytics systems. If this meets your needs, you only need to: 1. Turn it on when configuring the Adapty SDK by setting the `observerMode` parameter to `true`. Follow the setup instructions for [Flutter](sdk-installation-flutter#configure-adapty-sdk). 2. [Report transactions](report-transactions-observer-mode-flutter) from your existing purchase infrastructure to Adapty. ## Observer mode setup Turn on the Observer mode if you handle purchases and subscription status yourself and use Adapty for sending subscription events and analytics. :::important When running in Observer mode, Adapty SDK won't close any transactions, so make sure you're handling it. ::: ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withObserverMode(true) // Enable observer mode ..withLogLevel(AdaptyLogLevel.verbose), ); ``` Parameters: | Parameter | Description | | --------------------------- | ------------------------------------------------------------ | | observerMode | A boolean value that controls [Observer mode](observer-vs-full-mode). The default value is `false`. | ## Using Adapty paywalls in Observer Mode If you also want to use Adapty's paywalls and A/B testing features, you can — but it requires some extra setup in Observer mode. Here's what you'll need to do in addition to the steps above: 1. Display paywalls as usual for [remote config paywalls](present-remote-config-paywalls-flutter). 3. [Associate paywalls](report-transactions-observer-mode-flutter) with purchase transactions. --- # File: report-transactions-observer-mode-flutter --- --- title: "Report transactions in Observer Mode in Flutter SDK" description: "Report purchase transactions in Adapty Observer Mode for user insights and revenue tracking in Flutter SDK." --- In Observer mode, the Adapty SDK can't track purchases made through your existing purchase system on its own. You need to report transactions from your app store. It's crucial to set this up **before** releasing your app to avoid errors in analytics. Use `reportTransaction` to explicitly report each transaction for Adapty to recognize it. :::warning **Don't skip transaction reporting!** If you don't call `reportTransaction`, Adapty won't recognize the transaction, it won't appear in analytics, and it won't be sent to integrations. ::: If you use Adapty paywalls, include the `variationId` when reporting a transaction. This links the purchase to the paywall that triggered it, ensuring accurate paywall analytics. ```javascript showLineNumbers try { // every time when calling transaction.finish() await Adapty().reportTransaction( "YOUR_TRANSACTION_ID", variationId: "PAYWALL_VARIATION_ID", // optional ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` Parameters: | Parameter | Presence | Description | | ------------- | -------- | ------------------------------------------------------------ | | transactionId | required |
  • For iOS: Identifier of the transaction.
  • For Android: String identifier `purchase.getOrderId` of the purchase, where the purchase is an instance of the billing library [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) class.
| | variationId | optional | The string identifier of the variation. You can get it using `variationId` property of the [AdaptyPaywall](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html) object. |
In Observer mode, the Adapty SDK can't track purchases made through your existing purchase system on its own. You need to report transactions from your app store or restore them. It's crucial to set this up **before** releasing your app to avoid errors in analytics. Use `reportTransaction` on both platforms to explicitly report each transaction, and use `restorePurchases` on Android as an additional step to ensure Adapty recognizes it. :::warning **Don't skip transaction reporting and purchase restoring!** If you don't call these methods, Adapty won't recognize the transaction, it won't appear in analytics, and it won't be sent to integrations. ::: If you use Adapty paywalls, include the `variationId` when reporting a transaction. This links the purchase to the paywall that triggered it, ensuring accurate paywall analytics. ```javascript showLineNumbers // every time when calling transaction.finish() if (Platform.isAndroid) { try { await Adapty().restorePurchases(); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } } try { // every time when calling transaction.finish() await Adapty().reportTransaction( "YOUR_TRANSACTION_ID", variationId: "PAYWALL_VARIATION_ID", // optional ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` Parameters: | Parameter | Presence | Description | | ------------- | -------- | ------------------------------------------------------------ | | transactionId | required |
  • For iOS, StoreKit 1: an [SKPaymentTransaction](https://developer.apple.com/documentation/storekit/skpaymenttransaction) object.
  • For iOS, StoreKit 2: [Transaction](https://developer.apple.com/documentation/storekit/transaction) object.
  • For Android: String identifier (purchase.getOrderId of the purchase, where the purchase is an instance of the billing library [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) class.
| | variationId | optional | The string identifier of the variation. You can get it using `variationId` property of the [AdaptyPaywall](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html) object. |
**Reporting transactions** - Versions up to 3.1.x automatically listen for transactions in the App Store, so manual reporting is not required. - Version 3.2 does not support Observer Mode. **Reporting transactions** Use `restorePurchases` to report a transaction to Adapty in Observer Mode, as explained on the [Restore Purchases in Mobile Code](flutter-restore-purchase) page. :::warning **Don't skip transaction reporting!** If you don't call `restorePurchases`, Adapty won't recognize the transaction, it won't appear in analytics, and it won't be sent to integrations. ::: **Associating paywalls to transactions** Adapty SDK cannot determine the source of purchases, as you are the one processing them. Therefore, if you intend to use paywalls and/or A/B tests in Observer mode, you need to associate the transaction coming from your app store with the corresponding paywall in your mobile app code. This is important to get right before releasing your app, otherwise, it will lead to errors in analytics. ```javascript final transactionId = transaction.transactionIdentifier final variationId = paywall.variationId try { await Adapty().setVariationId('transactionId', variationId); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ```
--- # File: flutter-troubleshoot-purchases --- --- title: "Troubleshoot purchases in Flutter SDK" description: "Troubleshoot purchases in Flutter SDK" --- This guide helps you resolve common issues when implementing purchases manually in the Flutter SDK. ## makePurchase is called successfully, but the profile is not being updated **Issue**: The `makePurchase` method completes successfully, but the user's profile and subscription status are not updated in Adapty. **Reason**: This usually indicates incomplete Google Play Store setup or configuration issues. **Solution**: Ensure you've completed all the [Google Play setup steps](https://adapty.io/docs/initial-android). ## makePurchase is invoked twice **Issue**: The `makePurchase` method is being called multiple times for the same purchase. **Reason**: This typically happens when the purchase flow is triggered multiple times due to UI state management issues or rapid user interactions. **Solution**: Ensure you've completed all the [Google Play setup steps](https://adapty.io/docs/initial-android). ## AdaptyError.cantMakePayments in observer mode **Issue**: You're getting `AdaptyError.cantMakePayments` when using `makePurchase` in observer mode. **Reason**: In observer mode, you should handle purchases on your side, not use Adapty's `makePurchase` method. **Solution**: If you use `makePurchase` for purchases, turn off the observer mode. You need either to use `makePurchase` or handle purchases on your side in the observer mode. See [Implement Observer mode](implement-observer-mode-flutter) for more details. ## Adapty error: (code: 103, message: Play Market request failed on purchases updated: responseCode=3, debugMessage=Billing Unavailable, detail: null) **Issue**: You're receiving a billing unavailable error from Google Play Store. **Reason**: This error is not related to Adapty. It's a Google Play Billing Library error indicating that billing is not available on the device. **Solution**: This error is not related to Adapty. You can check and find out more about it in the Play Store documentation: [Handle BillingResult response codes](https://developer.android.com/google/play/billing/errors#billing_unavailable_error_code_3) | Play Billing | Android Developers. ## Not found makePurchasesCompletionHandlers **Issue**: You're encountering issues with `makePurchasesCompletionHandlers` not being found. **Reason**: This is typically related to sandbox testing issues. **Solution**: Create a new sandbox user and try again. This often resolves sandbox-related purchase completion handler issues. ## Other issues **Issue**: You're experiencing other purchase-related problems not covered above. **Solution**: Migrate the SDK to the latest version using the [migration guides](flutter-sdk-migration-guides) if needed. Many issues are resolved in newer SDK versions. --- # File: flutter-identifying-users --- --- title: "Identify users in Flutter SDK" description: "Identify users in Adapty to improve personalized subscription experiences." --- Adapty creates an internal profile ID for every user. However, if you have your own authentication system, you should set your own Customer User ID. You can find users by their Customer User ID in the [Profiles](profiles-crm) section and use it in the [server-side API](getting-started-with-server-side-api), which will be sent to all integrations. ### Setting customer user ID on configuration If you have a user ID during configuration, just pass it as `customerUserId` parameter to `.activate()` method: ```dart showLineNumbers title="Dart" try { await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_API_KEY') ..withCustomerUserId(YOUR_CUSTOMER_USER_ID) ); } catch (e) { // handle the error } ``` :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ### Setting customer user ID after configuration If you don't have a user ID in the SDK configuration, you can set it later at any time with the `.identify()` method. The most common cases for using this method are after registration or authorization, when the user switches from being an anonymous user to an authenticated user. ```dart showLineNumbers try { await Adapty().identify(customerUserId); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Request parameters: - **Customer User ID** (required): a string user identifier. :::warning Resubmitting of significant user data In some cases, such as when a user logs into their account again, Adapty's servers already have information about that user. In these scenarios, the Adapty SDK will automatically switch to work with the new user. If you passed any data to the anonymous user, such as custom attributes or attributions from third-party networks, you should resubmit that data for the identified user. It's also important to note that you should re-request all paywalls and products after identifying the user, as the new user's data may be different. ::: ### Logging out and logging in You can logout the user anytime by calling `.logout()` method: ```dart showLineNumbers try { await Adapty().logout(); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle unknown error } ``` You can then login the user using `.identify()` method. ## Assign `appAccountToken` (iOS) [`appAccountToken`](https://developer.apple.com/documentation/storekit/product/purchaseoption/appaccounttoken(_:)) is a **UUID** that lets you link App Store transactions to your internal user identity. StoreKit associates this token with every transaction, so your backend can match App Store data to your users. Use a stable UUID generated per user and reuse it for the same account across devices. This ensures that purchases and App Store notifications stay correctly linked. You can set the token in two ways – during the SDK activation or when identifying the user. :::important You must always pass `appAccountToken` together with `customerUserId`. If you pass only the token, it will not be included in the transaction. ::: ```dart showLineNumbers // During configuration: try { await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_API_KEY') ..withCustomerUserId(YOUR_CUSTOMER_USER_ID, iosAppAccountToken: "YOUR_APP_ACCOUNT_TOKEN") ); } catch (e) { // handle the error } // Or when identifying users try { await Adapty().identify(customerUserId, iosAppAccountToken: "YOUR_APP_ACCOUNT_TOKEN"); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ### Set obfuscated account IDs (Android) Google Play requires obfuscated account IDs for certain use cases to enhance user privacy and security. These IDs help Google Play identify purchases while keeping user information anonymous, which is particularly important for fraud prevention and analytics. You may need to set these IDs if your app handles sensitive user data or if you're required to comply with specific privacy regulations. The obfuscated IDs allow Google Play to track purchases without exposing actual user identifiers. ```dart showLineNumbers // During configuration: try { await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_API_KEY') ..withCustomerUserId(YOUR_CUSTOMER_USER_ID, androidObfuscatedAccountId: "OBFUSCATED_ACCOUNT_ID") ); } catch (e) { // handle the error } // Or when identifying users try { await Adapty().identify(customerUserId, androidObfuscatedAccountId: "OBFUSCATED_ACCOUNT_ID"); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` --- # File: flutter-setting-user-attributes --- --- title: "Set user attributes in Flutter SDK" description: "Learn how to set user attributes in Adapty to enable better audience segmentation." --- You can set optional attributes such as email, phone number, etc, to the user of your app. You can then use attributes to create user [segments](segments) or just view them in CRM. ### Setting user attributes To set user attributes, call `.updateProfile()` method: ```dart showLineNumbers final builder = AdaptyProfileParametersBuilder() ..setEmail("email@email.com") ..setPhoneNumber("+18888888888") ..setFirstName('John') ..setLastName('Appleseed') ..setGender(AdaptyProfileGender.other) ..setBirthday(DateTime(1970, 1, 3)); try { await Adapty().updateProfile(builder.build()); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Please note that the attributes that you've previously set with the `updateProfile` method won't be reset. :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ### The allowed keys list The allowed keys `` of `AdaptyProfileParameters.Builder` and the values `` are listed below: | Key | Value | |---|-----| |

email

phoneNumber

firstName

lastName

| String | | gender | Enum, allowed values are: `female`, `male`, `other` | | birthday | Date | ### Custom user attributes You can set your own custom attributes. These are usually related to your app usage. For example, for fitness applications, they might be the number of exercises per week, for language learning app user's knowledge level, and so on. You can use them in segments to create targeted paywalls and offers, and you can also use them in analytics to figure out which product metrics affect the revenue most. ```javascript showLineNumbers try { final builder = AdaptyProfileParametersBuilder() ..setCustomStringAttribute('value1', 'key1') ..setCustomDoubleAttribute(1.0, 'key2'); await Adapty().updateProfile(builder.build()); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` To remove existing key, use `.withRemoved(customAttributeForKey:)` method: ```javascript showLineNumbers try { final builder = AdaptyProfileParametersBuilder() ..removeCustomAttribute('key1') ..removeCustomAttribute('key2'); await Adapty().updateProfile(builder.build()); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Sometimes you need to figure out what custom attributes have already been installed before. To do this, use the `customAttributes` field of the `AdaptyProfile` object. :::warning Keep in mind that the value of `customAttributes` may be out of date since the user attributes can be sent from different devices at any time so the attributes on the server might have been changed after the last sync. ::: ### Limits - Up to 30 custom attributes per user - Key names are up to 30 characters long. The key name can include alphanumeric characters and any of the following: `_` `-` `.` - Value can be a string or float with no more than 50 characters. --- # File: flutter-listen-subscription-changes --- --- title: "Check subscription status in Flutter SDK" description: "Track and manage user subscription status in Adapty for improved customer retention in your Flutter app." --- With Adapty, keeping track of subscription status is made easy. You don't have to manually insert product IDs into your code. Instead, you can effortlessly confirm a user's subscription status by checking for an active [access level](access-level).
Before you start checking subscription status (Click to Expand) - For iOS, set up [App Store Server Notifications](enable-app-store-server-notifications) - For Android, set up [Real-time Developer Notifications (RTDN)](enable-real-time-developer-notifications-rtdn)
## Access level and the AdaptyProfile object Access levels are properties of the [AdaptyProfile](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyProfile-class.html) object. We recommend retrieving the profile when your app starts, such as when you [identify a user](flutter-identifying-users#setting-customer-user-id-on-configuration) , and then updating it whenever changes occur. This way, you can use the profile object without repeatedly requesting it. To be notified of profile updates, listen for profile changes as described in the [Listening for profile updates, including access levels](flutter-listen-subscription-changes.md) section below. :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ## Retrieving the access level from the server To get the access level from the server, use the `.getProfile()` method: ```javascript showLineNumbers try { final profile = await Adapty().getProfile(); // check the access } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Response parameters: | Parameter | Description | | --------- | ------------------------------------------------------------ | | Profile |

An [AdaptyProfile](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyProfile-class.html) object. Generally, you have to check only the access level status of the profile to determine whether the user has premium access to the app.

The `.getProfile` method provides the most up-to-date result as it always tries to query the API. If for some reason (e.g. no internet connection), the Adapty SDK fails to retrieve information from the server, the data from the cache will be returned. It is also important to note that the Adapty SDK updates `AdaptyProfile` cache regularly, to keep this information as up-to-date as possible.

| The `.getProfile()` method provides you with the user profile from which you can get the access level status. You can have multiple access levels per app. For example, if you have a newspaper app and sell subscriptions to different topics independently, you can create access levels "sports" and "science". But most of the time, you will only need one access level, in that case, you can just use the default "premium" access level. Here is an example for checking for the default "premium" access level: ```javascript showLineNumbers try { final profile = await Adapty().getProfile(); if (profile?.accessLevels['premium']?.isActive ?? false) { // grant access to premium features } } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ### Listening for subscription status updates Whenever the user's subscription changes, Adapty fires an event. To receive messages from Adapty, you need to make some additional configuration: ```javascript showLineNumbers Adapty().didUpdateProfileStream.listen((profile) { // handle any changes to subscription state }); ``` Adapty also fires an event at the start of the application. In this case, the cached subscription status will be passed. ### Subscription status cache The cache implemented in the Adapty SDK stores the subscription status of the profile. This means that even if the server is unavailable, the cached data can be accessed to provide information about the profile's subscription status. However, it's important to note that direct data requests from the cache are not possible. The SDK periodically queries the server every minute to check for any updates or changes related to the profile. If there are any modifications, such as new transactions or other updates, they will be sent to the cached data in order to keep it synchronized with the server. --- # File: flutter-deal-with-att --- --- title: "Deal with ATT in Flutter SDK" description: "Get started with Adapty on Flutter to streamline subscription setup and management." --- If your application uses AppTrackingTransparency framework and presents an app-tracking authorization request to the user, then you should send the [authorization status](https://developer.apple.com/documentation/apptrackingtransparency/attrackingmanager/authorizationstatus/) to Adapty. ```dart showLineNumbers final builder = AdaptyProfileParametersBuilder() ..setAppTrackingTransparencyStatus(AdaptyIOSAppTrackingTransparencyStatus.authorized); try { await Adapty().updateProfile(builder.build()); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle unknown error } ``` :::warning We strongly recommend that you send this value as early as possible when it changes, only in that case the data will be sent in a timely manner to the integrations you have configured. ::: --- # File: kids-mode-flutter --- --- title: "Kids Mode in Flutter SDK" description: "Easily enable Kids Mode to comply with Apple and Google policies. No IDFA, GAID, or ad data collected in Flutter SDK." --- If your Flutter application is intended for kids, you must follow the policies of [Apple](https://developer.apple.com/kids/) and [Google](https://support.google.com/googleplay/android-developer/answer/9893335). If you're using the Adapty SDK, a few simple steps will help you configure it to meet these policies and pass app store reviews. ## What's required? You need to configure the Adapty SDK to disable the collection of: - [IDFA (Identifier for Advertisers)](https://en.wikipedia.org/wiki/Identifier_for_Advertisers) (iOS) - [Android Advertising ID (AAID/GAID)](https://support.google.com/googleplay/android-developer/answer/6048248) (Android) - [IP address](https://www.ftc.gov/system/files/ftc_gov/pdf/p235402_coppa_application.pdf) In addition, we recommend using customer user ID carefully. User ID in format `` will be definitely treated as gathering personal data as well as using email. For Kids Mode, a best practice is to use randomized or anonymized identifiers (e.g., hashed IDs or device-generated UUIDs) to ensure compliance. ## Enabling Kids Mode ### Updates in the Adapty Dashboard In the Adapty Dashboard, you need to disable the IP address collection. To do this, go to [App settings](https://app.adapty.io/settings/general) and click **Disable IP address collection** under **Collect users' IP address**. ### Updates in your mobile app code In order to comply with policies, disable the collection of the user's IDFA (for iOS), GAID/AAID (for Android), and IP address. **Android: Update your SDK configuration** ```dart showLineNumbers title="Dart" try { await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_API_KEY') // highlight-start ..withGoogleAdvertisingIdCollectionDisabled(true), // set to `true` ..withIpAddressCollectionDisabled(true), // set to `true` // highlight-end ); } catch (e) { // handle the error } ``` **iOS: Enable Kids Mode using CocoaPods** 1. Update your Podfile: - If you **don't** have a `post_install` section, add the entire code block below. - If you **do** have a `post_install` section, merge the highlighted lines into it. ```ruby showLineNumbers title="Podfile" post_install do |installer| installer.pods_project.targets.each do |target| // highlight-start if target.name == 'Adapty' target.build_configurations.each do |config| config.build_settings['OTHER_SWIFT_FLAGS'] ||= ['$(inherited)'] config.build_settings['OTHER_SWIFT_FLAGS'] << '-DADAPTY_KIDS_MODE' end end // highlight-end end end ``` 2. Apply the changes by running ```sh showLineNumbers title="Shell" pod install ``` --- # File: flutter-get-onboardings --- --- title: "Get onboardings in Flutter SDK" description: "Learn how to retrieve onboardings in Adapty for Flutter." --- After [you designed the visual part for your onboarding](design-onboarding.md) with the builder in the Adapty Dashboard, you can display it in your Flutter app. The first step in this process is to get the onboarding associated with the placement and its view configuration as described below. Before you start, ensure that: 1. You have installed [Adapty Flutter SDK](sdk-installation-flutter.md) version 3.8.0 or higher. 2. You have [created an onboarding](create-onboarding.md). 3. You have added the onboarding to a [placement](placements.md). ## Fetch onboarding When you create an [onboarding](onboardings.md) with our no-code builder, it's stored as a container with configuration that your app needs to fetch and display. This container manages the entire experience - what content appears, how it's presented, and how user interactions (like quiz answers or form inputs) are processed. The container also automatically tracks analytics events, so you don't need to implement separate view tracking. For best performance, fetch the onboarding configuration early to give images enough time to download before showing to users. To get an onboarding, use the `getOnboarding` method: ```dart showLineNumbers try { final onboarding = await Adapty().getOnboarding(placementId: "YOUR_PLACEMENT_ID"); } on AdaptyError catch (e) { //handle error } catch (e) { //handle error } ``` Then, call the `createOnboardingView` method to get the view you will be displaying. :::warning The result of the `createOnboardingView` method can only be used once. If you need to use it again, call the `createOnboardingView` method anew. Calling it twice without recreating may result in the `AdaptyUIError.viewAlreadyPresented` error. ::: ```dart showLineNumbers try { final onboardingView = await Adapty().createOnboardingView(onboarding: onboarding); } on AdaptyError catch (e) { //handle error } catch (e) { //handle error } ``` Parameters: | Parameter | Presence | Description | |---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the onboarding localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

Adapty SDK stores onboardings locally in two layers: regularly updated cache described above and fallback onboardings. We also use CDN to fetch onboardings faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your onboardings while ensuring reliability even in cases where internet connection is scarce.

| | **loadTimeout** | default: 5 sec |

This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.

Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.

| Response parameters: | Parameter | Description | |:----------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------| | Onboarding | An [`AdaptyOnboarding`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyOnboarding-class.html) object with: the onboarding identifier and configuration, remote config, and several other properties. | ## Speed up onboarding fetching with default audience onboarding Typically, onboardings are fetched almost instantly, so you don't need to worry about speeding up this process. However, in cases where you have numerous audiences and onboardings, and your users have a weak internet connection, fetching a onboarding may take longer than you'd like. In such situations, you might want to display a default onboarding to ensure a smooth user experience rather than showing no onboarding at all. To address this, you can use the `getOnboardingForDefaultAudience` method, which fetches the onboarding of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the onboarding by the `getOnboarding` method, as detailed in the [Fetch Onboarding](#fetch-onboarding) section above. :::warning Consider using `getOnboarding` instead of `getOnboardingForDefaultAudience`, as the latter has important limitations: - **Compatibility issues**: May create problems when supporting multiple app versions, requiring either backward-compatible designs or accepting that older versions might display incorrectly. - **No personalization**: Only shows content for the "All Users" audience, removing targeting based on country, attribution, or custom attributes. If faster fetching outweighs these drawbacks for your use case, use `getOnboardingForDefaultAudience` as shown below. Otherwise, use `getOnboarding` as described [above](#fetch-onboarding). ::: ```dart showLineNumbers try { final onboarding = await Adapty().getOnboardingForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID'); } on AdaptyError catch (adaptyError) { // handle error } catch (e) { // handle unknown error } ``` Parameters: | Parameter | Presence | Description | |-----------------|-----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the onboarding localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

Adapty SDK stores onboardings locally in two layers: regularly updated cache described above and fallback onboardings. We also use CDN to fetch onboardings faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your onboardings while ensuring reliability even in cases where internet connection is scarce.

| --- # File: flutter-present-onboardings --- --- title: "Present onboardings in Flutter SDK" description: "Learn how to present onboardings effectively to drive more conversions." --- If you've customized an onboarding using the builder, you don't need to worry about rendering it in your Flutter app code to display it to the user. Such an onboarding contains both what should be shown within the onboarding and how it should be shown. Before you start, ensure that: 1. You have installed [Adapty Flutter SDK](sdk-installation-flutter.md) 3.8.0 or later. 2. You have [created an onboarding](create-onboarding.md). 3. You have added the onboarding to a [placement](placements.md). Adapty Flutter SDK provides two ways to present onboardings: - **Standalone screen** - **Embedded widget** ## Present as standalone screen To display an onboarding as a standalone screen, use the `onboardingView.present()` method on the `onboardingView` created by the `createOnboardingView` method. Each `view` can only be used once. If you need to display the onboarding again, call `createOnboardingView` one more time to create a new `onboardingView` instance. :::warning Reusing the same `onboardingView` without recreating it may result in an `AdaptyUIError.viewAlreadyPresented` error. ::: ```javascript showLineNumbers title="Flutter" try { await onboardingView.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ### Dismiss the onboarding When you need to programmatically close the onboarding, use the `dismiss()` method: ```dart showLineNumbers title="Flutter" try { await onboardingView.dismiss(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ### Configure iOS presentation style Configure how the onboarding is presented on iOS by passing the `iosPresentationStyle` parameter to the `present()` method. The parameter accepts `AdaptyUIIOSPresentationStyle.fullScreen` (default) or `AdaptyUIIOSPresentationStyle.pageSheet` values. ```dart showLineNumbers try { await onboardingView.present(iosPresentationStyle: AdaptyUIIOSPresentationStyle.pageSheet); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ## Embed in widget hierarchy To embed an onboarding within your existing widget tree, use the `AdaptyUIOnboardingPlatformView` widget directly in your Flutter widget hierarchy. ```javascript showLineNumbers title="Flutter" AdaptyUIOnboardingPlatformView( onboarding: onboarding, // The onboarding object you fetched onDidFinishLoading: (meta) { }, onDidFailWithError: (error) { }, onCloseAction: (meta, actionId) { }, onPaywallAction: (meta, actionId) { }, onCustomAction: (meta, actionId) { }, onStateUpdatedAction: (meta, elementId, params) { }, onAnalyticsEvent: (meta, event) { }, ) ``` :::note For Android platform view to work, ensure your `MainActivity` extends `FlutterFragmentActivity`: ```kotlin showLineNumbers title="Kotlin" class MainActivity : FlutterFragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } } ``` ::: ## Loader during onboarding When presenting an onboarding, you may notice a short loading screen between your splash screen and the onboarding while the underlying view is being initialized. You can handle this in different ways depending on your needs. #### Control splash screen using onDidFinishLoading :::note This approach is only available when embedding the onboarding as a widget. It is not available for standalone screen presentation. ::: The recommended cross-platform approach is to keep your splash screen or custom overlay visible until the onboarding is fully loaded, then hide it manually. When using the embedded widget, overlay your own widget above it and hide the overlay when `onDidFinishLoading` fires: ```dart showLineNumbers title="Flutter" AdaptyUIOnboardingPlatformView( onboarding: onboarding, onDidFinishLoading: (meta) { // Hide your custom splash screen or overlay here }, // ... other callbacks ) ``` ### Customize native loader :::important This approach is platform-specific and requires maintaining native UI code. It's not recommended unless you already maintain separate native layers in your app. ::: If you need to customize the default loader itself, you can replace it with platform-specific layouts. This approach requires separate implementations for Android and iOS: - **iOS**: Add `AdaptyOnboardingPlaceholderView.xib` to your Xcode project - **Android**: Create `adapty_onboarding_placeholder_view.xml` in `res/layout` and define a placeholder there ## Customize how links open in onboardings :::important Customizing how links open in onboardings is supported starting from Adapty SDK v.3.15.1. ::: By default, links in onboardings open in an in-app browser. This provides a seamless user experience by displaying web pages within your application, allowing users to view them without switching apps. If you prefer to open links in an external browser instead, you can customize this behavior by setting the `externalUrlsPresentation` parameter to `AdaptyWebPresentation.externalBrowser`: ```dart showLineNumbers title="Flutter" final onboardingView = await AdaptyUI().createOnboardingView( onboarding: onboarding, externalUrlsPresentation: AdaptyWebPresentation.externalBrowser, // default – AdaptyWebPresentation.inAppBrowser ); try { await onboardingView.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ```dart showLineNumbers title="Flutter" AdaptyUIOnboardingPlatformView( onboarding: onboarding, externalUrlsPresentation: AdaptyWebPresentation.externalBrowser, // default – AdaptyWebPresentation.inAppBrowser onDidFinishLoading: (meta) { }, onDidFailWithError: (error) { }, onCloseAction: (meta, actionId) { }, onPaywallAction: (meta, actionId) { }, onCustomAction: (meta, actionId) { }, onStateUpdatedAction: (meta, elementId, params) { }, onAnalyticsEvent: (meta, event) { }, ) ``` ## Disable safe area paddings (Android) By default, on Android devices, the onboarding view automatically applies safe area paddings to avoid system UI elements like status bar and navigation bar. However, if you want to disable this behavior and have full control over the layout, you can do so by adding a boolean resource to your app: 1. Go to `android/app/src/main/res/values`. If there is no `bools.xml` file, create it. 2. Add the following resource: ```xml false ``` Note that the changes apply globally for all onboardings in your app. --- # File: flutter-handling-onboarding-events --- --- title: "Handle onboarding events in Flutter SDK" description: "Handle onboarding-related events in Flutter using Adapty." --- Onboardings configured with the builder generate events your app can respond to. The way you handle these events depends on which presentation approach you're using: - **Full-screen presentation**: Requires setting up a global event observer that handles events for all onboarding views - **Embedded widget**: Handles events through inline callback parameters directly in the widget Before you start, ensure that: 1. You have installed [Adapty Flutter SDK](sdk-installation-flutter.md) 3.8.0 or later. 2. You have [created an onboarding](create-onboarding.md). 3. You have added the onboarding to a [placement](placements.md). ## Full-screen presentation events ### Set up event observer To handle events for full-screen onboardings, implement the `AdaptyUIOnboardingsEventsObserver` and set it before presenting: ```javascript showLineNumbers title="Flutter" AdaptyUI().setOnboardingsEventsObserver(this); try { await onboardingView.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ### Handle events Implement these methods in your observer: ```javascript showLineNumbers title="Flutter" void onboardingViewDidFinishLoading( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, ) { // Onboarding finished loading } void onboardingViewDidFailWithError( AdaptyUIOnboardingView view, AdaptyError error, ) { // Handle loading errors } void onboardingViewOnCloseAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { // Handle close action view.dismiss(); } void onboardingViewOnPaywallAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { // Dismiss onboarding before presenting paywall view.dismiss().then((_) { _openPaywall(actionId); }); } void onboardingViewOnCustomAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { // Handle custom actions } void onboardingViewOnStateUpdatedAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String elementId, AdaptyOnboardingsStateUpdatedParams params, ) { // Handle user input updates } void onboardingViewOnAnalyticsEvent( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, AdaptyOnboardingsAnalyticsEvent event, ) { // Track analytics events } ``` ## Embedded widget events When using `AdaptyUIOnboardingPlatformView`, you can handle events through inline callback parameters directly in the widget. Note that events will be sent to both the widget callbacks and the global observer (if set up), but the global observer is optional: ```javascript showLineNumbers title="Flutter" AdaptyUIOnboardingPlatformView( onboarding: onboarding, onDidFinishLoading: (meta) { // Onboarding finished loading }, onDidFailWithError: (error) { // Handle loading errors }, onCloseAction: (meta, actionId) { // Handle close action }, onPaywallAction: (meta, actionId) { _openPaywall(actionId); }, onCustomAction: (meta, actionId) { // Handle custom actions }, onStateUpdatedAction: (meta, elementId, params) { // Handle user input updates }, onAnalyticsEvent: (meta, event) { // Track analytics events }, ) ``` ## Event types The following sections describe the different types of events you can handle, regardless of which presentation approach you're using. ### Handle custom actions In the builder, you can add a **custom** action to a button and assign it an ID. Then, you can use this ID in your code and handle it as a custom action. For example, if a user taps a custom button, like **Login** or **Allow notifications**, the delegate method `onboardingController` will be triggered with the `.custom(id:)` case and the `actionId` parameter is the **Action ID** from the builder. You can create your own IDs, like "allowNotifications". ```javascript // Full-screen presentation void onboardingViewOnCustomAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { switch (actionId) { case 'login': _login(); break; case 'allow_notifications': _allowNotifications(); break; } } // Embedded widget onCustomAction: (meta, actionId) { _handleCustomAction(actionId); } ```
Event example (Click to expand) ```json { "actionId": "allowNotifications", "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "screensTotal": 3 } } ```
### Finishing loading onboarding When an onboarding finishes loading, this event will be triggered: ```javascript showLineNumbers title="Flutter" // Full-screen presentation void onboardingViewDidFinishLoading( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, ) { print('Onboarding loaded: ${meta.onboardingId}'); } // Embedded widget onDidFinishLoading: (meta) { print('Onboarding loaded: ${meta.onboardingId}'); } ```
Event example (Click to expand) ```json { "meta": { "onboarding_id": "onboarding_123", "screen_cid": "welcome_screen", "screen_index": 0, "total_screens": 4 } } ```
### Closing onboarding Onboarding is considered closed when a user taps a button with the **Close** action assigned. :::important Note that you need to manage what happens when a user closes the onboarding. For instance, you need to stop displaying the onboarding itself. ::: ```javascript showLineNumbers title="Flutter" // Full-screen presentation void onboardingViewOnCloseAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { await view.dismiss(); } // Embedded widget onCloseAction: (meta, actionId) { Navigator.of(context).pop(); } ```
Event example (Click to expand) ```json { "action_id": "close_button", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "final_screen", "screen_index": 3, "total_screens": 4 } } ```
### Opening a paywall :::tip Handle this event to open a paywall if you want to open it inside the onboarding. If you want to open a paywall after it is closed, there is a more straightforward way to do it – handle the close action and open a paywall without relying on the event data. ::: If a user clicks a button that opens a paywall, you will get a button action ID that you [set up manually](get-paid-in-onboardings.md). The most seamless way to work with paywalls in onboardings is to make the action ID equal to a paywall placement ID: Note that, for iOS, only one view (paywall or onboarding) can be displayed on screen at a time. If you present a paywall on top of an onboarding, you cannot programmatically control the onboarding in the background. Attempting to dismiss the onboarding will close the paywall instead, leaving the onboarding visible. To avoid this, always dismiss the onboarding view before presenting the paywall. ```javascript showLineNumbers title="Flutter" // Full-screen presentation void onboardingViewOnPaywallAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { // Dismiss onboarding before presenting paywall view.dismiss().then((_) { _openPaywall(actionId); }); } Future _openPaywall(String actionId) async { // Implement your paywall opening logic here } // Embedded widget onPaywallAction: (meta, actionId) { _openPaywall(actionId); } ```
Event example (Click to expand) ```json { "action_id": "premium_offer_1", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "pricing_screen", "screen_index": 2, "total_screens": 4 } } ```
### Tracking navigation You receive an analytics event when various navigation-related events occur during the onboarding flow: ```javascript showLineNumbers title="Flutter" // Full-screen presentation void onboardingViewOnAnalyticsEvent( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, AdaptyOnboardingsAnalyticsEvent event, ) { trackEvent(event.type, meta.onboardingId); } // Embedded widget onAnalyticsEvent: (meta, event) { trackEvent(event.type, meta.onboardingId); } ``` The `event` object can be one of the following types: |Type | Description | |------------|-------------| | `onboardingStarted` | When the onboarding has been loaded | | `screenPresented` | When any screen is shown | | `screenCompleted` | When a screen is completed. Includes optional `elementId` (identifier of the completed element) and optional `reply` (response from the user). Triggered when users perform any action to exit the screen. | | `secondScreenPresented` | When the second screen is shown | | `userEmailCollected` | Triggered when the user's email is collected via the input field | | `onboardingCompleted` | Triggered when a user reaches a screen with the `final` ID. If you need this event, [assign the `final` ID to the last screen](design-onboarding.md). | | `unknown` | For any unrecognized event type. Includes `name` (the name of the unknown event) and `meta` (additional metadata) | Each event includes `meta` information containing: | Field | Description | |------------|-------------| | `onboardingId` | Unique identifier of the onboarding flow | | `screenClientId` | Identifier of the current screen | | `screenIndex` | Current screen's position in the flow | | `screensTotal` | Total number of screens in the flow |
Event examples (Click to expand) ```javascript // onboardingStarted { "name": "onboarding_started", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "welcome_screen", "screen_index": 0, "total_screens": 4 } } // screenPresented { "name": "screen_presented", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "interests_screen", "screen_index": 2, "total_screens": 4 } } // screenCompleted { "name": "screen_completed", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 1, "total_screens": 4 }, "params": { "element_id": "profile_form", "reply": "success" } } // secondScreenPresented { "name": "second_screen_presented", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 1, "total_screens": 4 } } // userEmailCollected { "name": "user_email_collected", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 1, "total_screens": 4 } } // onboardingCompleted { "name": "onboarding_completed", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "final_screen", "screen_index": 3, "total_screens": 4 } } ```
--- # File: flutter-onboarding-input --- --- title: "Process data from onboardings in Flutter SDK" description: "Save and use data from onboardings in your Flutter app with Adapty SDK." --- When your users respond to a quiz question or input their data into an input field, the `onStateUpdatedAction` method will be invoked. You can save or process the field type in your code. For example: ```dart // Full-screen presentation void onboardingViewOnStateUpdatedAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String elementId, AdaptyOnboardingsStateUpdatedParams params, ) { // Process data } // Embedded widget onStateUpdatedAction: (meta, elementId, params) { // Process data } ``` See the action format [here](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyUIOnboardingPlatformView/onStateUpdatedAction.html).
Saved data examples (the format may differ in your implementation) ```javascript // Example of a saved select action { "elementId": "preference_selector", "meta": { "onboardingId": "onboarding_123", "screenClientId": "preferences_screen", "screenIndex": 1, "screensTotal": 3 }, "params": { "type": "select", "value": { "id": "option_1", "value": "premium", "label": "Premium Plan" } } } // Example of a saved multi-select action { "elementId": "interests_selector", "meta": { "onboardingId": "onboarding_123", "screenClientId": "interests_screen", "screenIndex": 2, "screensTotal": 3 }, "params": { "type": "multiSelect", "value": [ { "id": "interest_1", "value": "sports", "label": "Sports" }, { "id": "interest_2", "value": "music", "label": "Music" } ] } } // Example of a saved input action { "elementId": "name_input", "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "screensTotal": 3 }, "params": { "type": "input", "value": { "type": "text", "value": "John Doe" } } } // Example of a saved date picker action { "elementId": "birthday_picker", "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "screensTotal": 3 }, "params": { "type": "datePicker", "value": { "day": 15, "month": 6, "year": 1990 } } } ```
## Use cases ### Enrich user profiles with data If you want to immediately link the input data with the user profile and avoid asking them twice for the same info, you need to [update the user profile](flutter-setting-user-attributes.md) with the input data when handling the action. For example, you ask users to enter their name in the text field with the `name` ID, and you want to set this field's value as user's first name. Also, you ask them to enter their email in the `email` field. In your app code, it can look like this: ```dart showLineNumbers // Full-screen presentation void onboardingViewOnStateUpdatedAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String elementId, AdaptyOnboardingsStateUpdatedParams params, ) { // Store user preferences or responses if (params is AdaptyOnboardingsInputParams) { final builder = AdaptyProfileParametersBuilder(); // Map elementId to appropriate profile field switch (elementId) { case 'name': if (params.input is AdaptyOnboardingsTextInput) { builder.setFirstName((params.input as AdaptyOnboardingsTextInput).value); } break; case 'email': if (params.input is AdaptyOnboardingsEmailInput) { builder.setEmail((params.input as AdaptyOnboardingsEmailInput).value); } break; } // Update profile Adapty().updateProfile(builder.build()).catchError((error) { // handle the error }); } } // Embedded widget onStateUpdatedAction: (meta, elementId, params) { // Store user preferences or responses if (params is AdaptyOnboardingsInputParams) { final builder = AdaptyProfileParametersBuilder(); // Map elementId to appropriate profile field switch (elementId) { case 'name': if (params.input is AdaptyOnboardingsTextInput) { builder.setFirstName((params.input as AdaptyOnboardingsTextInput).value); } break; case 'email': if (params.input is AdaptyOnboardingsEmailInput) { builder.setEmail((params.input as AdaptyOnboardingsEmailInput).value); } break; } // Update profile Adapty().updateProfile(builder.build()).catchError((error) { // handle the error }); } } ``` ### Customize paywalls based on answers Using quizzes in onboardings, you can also customize paywalls you show users after they complete the onboarding. For example, you can ask users about their experience with sport and show different CTAs and products to different user groups. 1. [Add a quiz](onboarding-quizzes.md) in the onboarding builder and assign meaningful IDs to its options. 2. Handle the quiz responses based on their IDs and [set custom attributes](flutter-setting-user-attributes.md) for users. ```dart showLineNumbers // Full-screen presentation void onboardingViewOnStateUpdatedAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String elementId, AdaptyOnboardingsStateUpdatedParams params, ) { // Handle quiz responses and set custom attributes if (params is AdaptyOnboardingsSelectParams) { final builder = AdaptyProfileParametersBuilder(); // Map quiz responses to custom attributes switch (elementId) { case 'experience': // Set custom attribute 'experience' with the selected value (beginner, amateur, pro) builder.setCustomStringAttribute(params.value, 'experience'); break; } // Update profile Adapty().updateProfile(builder.build()).catchError((error) { // handle the error }); } } // Embedded widget onStateUpdatedAction: (meta, elementId, params) { // Handle quiz responses and set custom attributes if (params is AdaptyOnboardingsSelectParams) { final builder = AdaptyProfileParametersBuilder(); // Map quiz responses to custom attributes switch (elementId) { case 'experience': // Set custom attribute 'experience' with the selected value (beginner, amateur, pro) builder.setCustomStringAttribute(params.value, 'experience'); break; } // Update profile Adapty().updateProfile(builder.build()).catchError((error) { // handle the error }); } } ``` 3. [Create segments](segments.md) for each custom attribute value. 4. Create a [placement](placements.md) and add [audiences](audience.md) for each segment you've created. 5. [Display a paywall](flutter-paywalls.md) for the placement in your app code. If your onboarding has a button that opens a paywall, implement the paywall code as a [response to this button's action](flutter-handling-onboarding-events#opening-a-paywall). --- # File: flutter-test --- --- title: "Test & release in Flutter SDK" description: "Learn how to check subscription status in your Flutter app with Adapty." --- If you've already implemented the Adapty SDK in your Flutter app, you'll want to test that everything is set up correctly and that purchases work as expected across both iOS and Android platforms. This involves testing both the SDK integration and the actual purchase flow with Apple's sandbox environment and Google Play's testing environment. ## Test your app For comprehensive testing of your in-app purchases, see our platform-specific testing guides: [iOS testing guide](test-purchases-in-sandbox.md) and [Android testing guide](testing-on-android.md). ## Prepare for release Before submitting your app to the store, follow the [Release checklist](release-checklist) to confirm: - Store connection and server notifications are configured - Purchases complete and are reported to Adapty - Access unlocks and restores correctly - Privacy and review requirements are met --- # File: InvalidProductIdentifiers-flutter --- ``` --- title: "Fix for Code-1000 noProductIDsFound error in Flutter SDK" description: "Resolve invalid product identifier errors when managing subscriptions in Adapty." metadataTitle: "Fix for Code-1000 noProductIDsFound in Flutter | Adapty Docs" slug: "InvalidProductIdentifiers-flutter" --- The 1000-code error, `noProductIDsFound`, indicates that none of the products you requested on the paywall are available for purchase in the App Store, even though they're listed there. This error may sometimes come with an `InvalidProductIdentifiers` warning. If the warning appears without an error, safely ignore it. If you're encountering the `noProductIDsFound` error, follow these steps to resolve it: ## Step 1. Check bundle ID \{#step-2-check-bundle-id\} --- no_index: true --- 1. Open [App Store Connect](https://appstoreconnect.apple.com/apps). Select your app and proceed to **General** → **App Information** section. 2. Copy the **Bundle ID** in the **General Information** sub-section. 3. Open the [**App settings** -> **iOS SDK** tab](https://app.adapty.io/settings/ios-sdk) from the Adapty top menu and paste the copied value to the **Bundle ID** field. 4. Get back to the **App information** page in App Store Connect and copy **Apple ID** from there. 5. On the [**App settings** -> **iOS SDK**](https://app.adapty.io/settings/ios-sdk) page in the Adapty dashboard, paste the ID to the **Apple app ID** field. ## Step 2. Check products \{#step-3-check-products\} 1. Go to **App Store Connect** and navigate to [**Monetization** → **Subscriptions**](https://appstoreconnect.apple.com/apps/6477523342/distribution/subscriptions) in the left-hand menu. 2. Click on the subscription group name. You'll see your products listed under the **Subscriptions** section. 3. Ensure the product you're testing is marked **Ready to Submit**. 4. Compare the product ID from the table with the one in the [**Products**](https://app.adapty.io/products) tab in the Adapty Dashboard. If the IDs don't match, copy the product ID from the table and [create a product](create-product) with it in the Adapty Dashboard. ## Step 3. Check product availability \{#step-4-check-product-availability\} 1. Go back to **App Store Connect** and open the same **Subscriptions** section. 2. Click the subscription group name to view your products. 3. Select the product you're testing. 4. Scroll to the **Availability** section and check that all the required countries and regions are listed. ## Step 4. Check product prices \{#step-5-check-product-prices\} 1. Again, head to the **Monetization** → **Subscriptions** section in **App Store Connect**. 2. Click the subscription group name. 3. Select the product you're testing. 4. Scroll down to **Subscription Pricing** and expand the **Current Pricing for New Subscribers** section. 5. Ensure that all required prices are listed. ## Step 5. Check app paid status, bank account, and tax forms are active 1. In **App Store Connect**](https://appstoreconnect.apple.com/) homepage, click **Business**. 2. Select your company name. 3. Scroll down and check that your **Paid Apps Agreement**, **Bank Account**, and **Tax forms** all show as **Active**. By following these steps, you should be able to resolve the `InvalidProductIdentifiers` warning and get your products live in the store --- # File: cantMakePayments-flutter --- --- title: "Fix for Code-1003 cantMakePayment error in Flutter SDK" description: "Resolve making payments error when managing subscriptions in Adapty." --- The 1003 error, `cantMakePayments`, indicates that in-app purchases can't be made on this device. If you’re encountering the `cantMakePayments` error, this is usually due to one of the reasons: - Device restrictions: The error is not related to Adapty. See the ways to fix the issue below. - Observer mode configuration: The `makePurchase` method and the observer mode can't be used at the same time. See the section below. ## Issue: Device restrictions | Issue | Solution | |---------------------------|---------------------------------------------------------| | Screen Time restrictions | Disable In-App Purchase restrictions in [Screen Time](https://support.apple.com/en-us/102470) | | Account suspended | Contact Apple Support to resolve account issues | | Regional restrictions | Use App Store account from supported region | ## Issue: Using both Observer mode and makePurchase If you are using `makePurchases` to handle purchases, you don't need to use Observer mode. [Observer mode](https://adapty.io/docs/observer-vs-full-mode) is only needed if you implement the purchase logic yourself. So, if you're using `makePurchase`, you can safely remove enabling Observer mode from the SDK activation code. --- # File: flutter-migration-guide-310 --- --- title: "Migration guide to Flutter Adapty SDK 3.10.0" description: "" --- Adapty SDK 3.10.0 is a major release that brought some improvements that however may require some migration steps from you: 1. Update the `makePurchase` method to use `AdaptyPurchaseParameters` instead of individual parameters. 2. Replace `vendorProductIds` with `productIdentifiers` in the `AdaptyPaywall` model. ## Update makePurchase method The `makePurchase` method now uses `AdaptyPurchaseParameters` instead of individual `subscriptionUpdateParams` and `isOfferPersonalized` arguments. This provides better type safety and allows for future extensibility of purchase parameters. ```diff showLineNumbers - final purchaseResult = await adapty.makePurchase( - product: product, - subscriptionUpdateParams: subscriptionUpdateParams, - isOfferPersonalized: true, - ); + final parameters = AdaptyPurchaseParametersBuilder() + ..setSubscriptionUpdateParams(subscriptionUpdateParams) + ..setIsOfferPersonalized(true) + ..setObfuscatedAccountId('your-account-id') + ..setObfuscatedProfileId('your-profile-id'); + final purchaseResult = await adapty.makePurchase( + product: product, + parameters: parameters.build(), + ); ``` If no additional parameters are needed, you can simply use: ```dart showLineNumbers final purchaseResult = await adapty.makePurchase( product: product, ); ``` ## Update AdaptyPaywall model usage The `vendorProductIds` property has been deprecated in favor of `productIdentifiers`. The new property returns `AdaptyProductIdentifier` objects instead of simple strings, providing more structured product information. ```diff showLineNumbers - paywall.vendorProductIds.map((vendorId) => - ListTextTile(title: vendorId) - ).toList() + paywall.productIdentifiers.map((productId) => + ListTextTile(title: productId.vendorProductId) + ).toList() ``` The `AdaptyProductIdentifier` object provides access to the vendor product ID through the `vendorProductId` property, maintaining the same functionality while offering better structure for future enhancements. ## Backward compatibility Both changes maintain backward compatibility: - The old parameters in `makePurchase` are deprecated but still functional - The `vendorProductIds` property is deprecated but still accessible - Existing code will continue to work, though you'll see deprecation warnings We recommend updating your code to use the new APIs to ensure future compatibility and take advantage of the improved type safety and extensibility. --- # File: flutter-migration-guide-38 --- --- title: "Migrate Adapty Flutter SDK to v. 3.8" description: "Migrate to Adapty Flutter SDK v3.8 for better performance and new monetization features." --- Adapty SDK 3.8.0 is a major release that brought some improvements which however may require some migration steps from you. 1. Update the observer class and method names. 2. Update the fallback paywalls method name. 3. Update the view class name in event handling methods. ## Update observer class and method names The observer class and its registration method have been renamed: ```diff showLineNumbers - class MyObserver extends AdaptyUIObserver { + class MyObserver extends AdaptyUIPaywallsEventsObserver { @override void paywallViewDidPerformAction(AdaptyUIView view, AdaptyUIAction action) { // Handle action } } // Register observer - AdaptyUI().setObserver(this); + AdaptyUI().setPaywallsEventsObserver(this); ``` ## Update fallback paywalls method name The method for setting fallback paywalls has been simplified: ```diff showLineNumbers try { - await Adapty.setFallbackPaywalls(assetId); + await Adapty.setFallback(assetId); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` ## Update view class name in event handling methods All event handling methods now use the new `AdaptyUIPaywallView` class instead of `AdaptyUIView`: ```diff showLineNumbers - void paywallViewDidPerformAction(AdaptyUIView view, AdaptyUIAction action) + void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) - void paywallViewDidSelectProduct(AdaptyUIView view, AdaptyPaywallProduct product) + void paywallViewDidSelectProduct(AdaptyUIPaywallView view, AdaptyPaywallProduct product) - void paywallViewDidStartPurchase(AdaptyUIView view, AdaptyPaywallProduct product) + void paywallViewDidStartPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product) - void paywallViewDidFinishPurchase(AdaptyUIView view, AdaptyPaywallProduct product, AdaptyProfile profile) + void paywallViewDidFinishPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyProfile profile) - void paywallViewDidFailPurchase(AdaptyUIView view, AdaptyPaywallProduct product, AdaptyError error) + void paywallViewDidFailPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyError error) - void paywallViewDidFinishRestore(AdaptyUIView view, AdaptyProfile profile) + void paywallViewDidFinishRestore(AdaptyUIPaywallView view, AdaptyProfile profile) - void paywallViewDidFailRestore(AdaptyUIView view, AdaptyError error) + void paywallViewDidFailRestore(AdaptyUIPaywallView view, AdaptyError error) - void paywallViewDidFailLoadingProducts(AdaptyUIView view, AdaptyIOSProductsFetchPolicy? fetchPolicy, AdaptyError error) + void paywallViewDidFailLoadingProducts(AdaptyUIPaywallView view, AdaptyIOSProductsFetchPolicy? fetchPolicy, AdaptyError error) - void paywallViewDidFailRendering(AdaptyUIView view, AdaptyError error) + void paywallViewDidFailRendering(AdaptyUIPaywallView view, AdaptyError error) ``` --- # File: migration-to-flutter-sdk-34 --- --- title: "Migrate Adapty Flutter SDK to v. 3.4" description: "Migrate to Adapty Flutter SDK v3.4 for better performance and new monetization features." --- Adapty SDK 3.4.0 is a major release that introduces improvements that require migration steps on your end. ## Update fallback paywall files Update your fallback paywall files to ensure compatibility with the new SDK version: 1. [Download the updated fallback paywall files](fallback-paywalls#download-fallback-paywalls-as-a-file-in-the-adapty-dashboard) from the Adapty Dashboard. 2. [Replace the existing fallback paywalls in your mobile app](flutter-use-fallback-paywalls) with the new files. ## Update implementation of Observer Mode If you're using Observer Mode, make sure to update its implementation. Previously, different methods were used to report transactions to Adapty. In the new version, the `reportTransaction` method should be used consistently across both Android and iOS. This method explicitly reports each transaction to Adapty, ensuring it's recognized. If a paywall was used, pass the variation ID to link the transaction to it. :::warning **Don't skip transaction reporting!** If you don't call `reportTransaction`, Adapty won't recognize the transaction, it won't appear in analytics, and it won't be sent to integrations. ::: ```diff showLineNumbers - // every time when calling transaction.finish() - if (Platform.isAndroid) { - try { - await Adapty().restorePurchases(); - } on AdaptyError catch (adaptyError) { - // handle the error - } catch (e) { - } - } try { // every time when calling transaction.finish() await Adapty().reportTransaction( "YOUR_TRANSACTION_ID", variationId: "PAYWALL_VARIATION_ID", // optional ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` --- # File: migration-to-flutter330 --- --- title: "Migrate Adapty Flutter SDK to v. 3.3" description: "Migrate to Adapty Flutter SDK v3.3 for better performance and new monetization features." --- Adapty SDK 3.3.0 is a major release that brought some improvements which however may require some migration steps from you. 1. Update the method for providing fallback paywalls. 2. Remove `getProductsIntroductoryOfferEligibility` method. 3. Update integration configurations for Adjust, AirBridge, Amplitude, AppMetrica, Appsflyer, Branch, Facebook Ads, Firebase and Google Analytics, Mixpanel, OneSignal, Pushwoosh. 4. Update Observer mode implementation. ## Update method for providing fallback paywalls Previously, the method required the fallback paywall as a JSON string (`jsonString`), but now it takes the path to the local fallback file (`assetId`) instead. ```diff showLineNumbers import 'dart:async' show Future; import 'dart:io' show Platform; -import 'package:flutter/services.dart' show rootBundle; -final filePath = Platform.isIOS ? 'assets/ios_fallback.json' : 'assets/android_fallback.json'; -final jsonString = await rootBundle.loadString(filePath); +final assetId = Platform.isIOS ? 'assets/ios_fallback.json' : 'assets/android_fallback.json'; try { - await adapty.setFallbackPaywalls(jsonString); + await adapty.setFallbackPaywalls(assetId); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` For the complete code example, check out the [Use fallback paywalls](flutter-use-fallback-paywalls) page. ## Remove `getProductsIntroductoryOfferEligibility` method Before Adapty iOS SDK 3.3.0, the product object always included offers, regardless of whether the user was eligible. You had to manually check eligibility before using the offer. Now, the product object only includes an offer if the user is eligible. This means you no longer need to check eligibility — if an offer is present, the user is eligible. ## Update third-party integration SDK configuration To ensure integrations work properly with Adapty Flutter SDK 3.3.0 and later, update your SDK configurations for the following integrations as described in the sections below. ### Adjust Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Adjust integration](adjust#sdk-configuration). ```diff showLineNumbers import 'package:adjust_sdk/adjust.dart'; import 'package:adjust_sdk/adjust_config.dart'; try { final adid = await Adjust.getAdid(); if (adid == null) { // handle the error } + await Adapty().setIntegrationIdentifier( + key: "adjust_device_id", + value: adid, + ); final attributionData = await Adjust.getAttribution(); var attribution = Map(); if (attributionData.trackerToken != null) attribution['trackerToken'] = attributionData.trackerToken!; if (attributionData.trackerName != null) attribution['trackerName'] = attributionData.trackerName!; if (attributionData.network != null) attribution['network'] = attributionData.network!; if (attributionData.adgroup != null) attribution['adgroup'] = attributionData.adgroup!; if (attributionData.creative != null) attribution['creative'] = attributionData.creative!; if (attributionData.clickLabel != null) attribution['clickLabel'] = attributionData.clickLabel!; if (attributionData.costType != null) attribution['costType'] = attributionData.costType!; if (attributionData.costAmount != null) attribution['costAmount'] = attributionData.costAmount!.toString(); if (attributionData.costCurrency != null) attribution['costCurrency'] = attributionData.costCurrency!; if (attributionData.fbInstallReferrer != null) attribution['fbInstallReferrer'] = attributionData.fbInstallReferrer!; - Adapty().updateAttribution( - attribution, - source: AdaptyAttributionSource.adjust, - networkUserId: adid, - ); + await Adapty().updateAttribution(attribution, source: "adjust"); } catch (e) { // handle the error } on AdaptyError catch (adaptyError) { // handle the error } ``` ### AirBridge Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for AirBridge integration](airbridge#sdk-configuration). ```diff showLineNumbers import 'package:airbridge_flutter_sdk/airbridge_flutter_sdk.dart'; final deviceUUID = await Airbridge.state.deviceUUID; try { - final builder = AdaptyProfileParametersBuilder() - ..setAirbridgeDeviceId(deviceUUID); - await Adapty().updateProfile(builder.build()); + await Adapty().setIntegrationIdentifier( + key: "airbridge_device_id", + value: deviceUUID, + ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` ### Amplitude Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Amplitude integration](amplitude#sdk-configuration). ```diff showLineNumbers import 'package:amplitude_flutter/amplitude.dart'; final Amplitude amplitude = Amplitude.getInstance(instanceName: "YOUR_INSTANCE_NAME"); final deviceId = await amplitude.getDeviceId(); final userId = await amplitude.getUserId(); try { - final builder = AdaptyProfileParametersBuilder() - ..setAmplitudeDeviceId(deviceId) - ..setAmplitudeUserId(userId); - await adapty.updateProfile(builder.build()); + await Adapty().setIntegrationIdentifier( + key: "amplitude_user_id", + value: userId, + ); + await Adapty().setIntegrationIdentifier( + key: "amplitude_device_id", + value: deviceId, + ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` ### AppMetrica Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for AppMetrica integration](appmetrica#sdk-configuration). ```diff showLineNumbers import 'package:appmetrica_plugin/appmetrica_plugin.dart'; final deviceId = await AppMetrica.deviceId; if (deviceId != null) { try { - final builder = AdaptyProfileParametersBuilder() - ..setAppmetricaDeviceId(deviceId) - ..setAppmetricaProfileId("YOUR_ADAPTY_CUSTOMER_USER_ID"); - - await adapty.updateProfile(builder.build()); + await Adapty().setIntegrationIdentifier( + key: "appmetrica_device_id", + value: deviceId, + ); + await Adapty().setIntegrationIdentifier( + key: "appmetrica_profile_id", + value: "YOUR_ADAPTY_CUSTOMER_USER_ID", + ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } } ``` ### AppsFlyer Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for AppsFlyer integration](appsflyer#sdk-configuration). ```diff showLineNumbers import 'package:appsflyer_sdk/appsflyer_sdk.dart'; AppsflyerSdk appsflyerSdk = AppsflyerSdk(); appsflyerSdk.onInstallConversionData((data) async { try { final appsFlyerUID = await appsFlyerSdk.getAppsFlyerUID(); - await Adapty().updateAttribution( - data, - source: AdaptyAttributionSource.appsflyer, - networkUserId: appsFlyerUID, - ); + await Adapty().setIntegrationIdentifier( + key: "appsflyer_id", + value: appsFlyerUID, + ); + + await Adapty().updateAttribution(data, source: "appsflyer"); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } }); appsflyerSdk.initSdk( registerConversionDataCallback: true, registerOnAppOpenAttributionCallback: true, registerOnDeepLinkingCallback: true, ); ``` ### Branch Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Branch integration](branch#sdk-configuration). ```diff showLineNumbers FlutterBranchSdk.initSession().listen((data) async { try { + await Adapty().setIntegrationIdentifier( + key: "branch_id", + value: , + ); - await Adapty().updateAttribution(data, source: AdaptyAttributionSource.branch); + await Adapty().updateAttribution(data, source: "branch"); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ); ``` ### Firebase and Google Analytics Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Firebase and Google Analytics integration](firebase-and-google-analytics). ```diff showLineNumbers final appInstanceId = await FirebaseAnalytics.instance.appInstanceId; try { - final builder = AdaptyProfileParametersBuilder() - ..setFirebaseAppInstanceId(appInstanceId); - await adapty.updateProfile(builder.build()); + await Adapty().setIntegrationIdentifier( + key: "firebase_app_instance_id", + value: appInstanceId, + ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` ### Mixpanel Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Mixpanel integration](mixpanel#sdk-configuration). ```diff showLineNumbers final mixpanel = await Mixpanel.init("Your Token", trackAutomaticEvents: true); final distinctId = await mixpanel.getDistinctId(); try { - final builder = AdaptyProfileParametersBuilder() - ..setMixpanelUserId(distinctId); - await Adapty().updateProfile(builder.build()); + await Adapty().setIntegrationIdentifier( + key: "mixpanel_user_id", + value: distinctId, + ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` ### OneSignal Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for OneSignal integration](onesignal#sdk-configuration). ```diff showLineNumbers OneSignal.shared.setSubscriptionObserver((changes) { final playerId = changes.to.userId; if (playerId != null) { - final builder = - AdaptyProfileParametersBuilder() - ..setOneSignalPlayerId(playerId); - // ..setOneSignalSubscriptionId(playerId); try { - Adapty().updateProfile(builder.build()); + await Adapty().setIntegrationIdentifier( + key: "one_signal_player_id", + value: playerId, + ); } on AdaptyError catch (adaptyError) { // handle error } catch (e) { // handle error } } }); ``` ### Pushwoosh Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Pushwoosh integration](pushwoosh#sdk-configuration). ```diff showLineNumbers final hwid = await Pushwoosh.getInstance.getHWID; - final builder = AdaptyProfileParametersBuilder() - ..setPushwooshHWID(hwid); try { - await adapty.updateProfile(builder.build()); + await Adapty().setIntegrationIdentifier( + key: "pushwoosh_hwid", + value: hwid, + ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` ## Update Observer mode implementation Update how you link paywalls to transactions. Previously, you used the `setVariationId` method to assign the `variationId`. Now, you can include the `variationId` directly when recording the transaction using the new `reportTransaction` method. Check out the final code example in the [Associate paywalls with purchase transactions in Observer mode](report-transactions-observer-mode-flutter.md). :::warning Don't forget to record the transaction using the `reportTransaction` method. Skipping this step means Adapty won't recognize the transaction, won't grant access levels, won't include it in analytics, and won't send it to integrations. This step is essential! ::: ```diff showLineNumbers try { - await Adapty().setVariationId("YOUR_TRANSACTION_ID", "PAYWALL_VARIATION_ID"); + // every time when calling transaction.finish() + await Adapty().reportTransaction( + "YOUR_TRANSACTION_ID", + variationId: "PAYWALL_VARIATION_ID", // optional + ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` --- # File: migration-to-flutter-sdk-v3 --- --- title: "Migrate Adapty Flutter SDK to v. 3.0" description: "Migrate to Adapty Flutter SDK v3.0 for better performance and new monetization features." --- Adapty SDK v.3.0 brings support for the new exciting [Adapty Paywall Builder](adapty-paywall-builder), the new version of the no-code user-friendly tool to create paywalls. With its maximum flexibility and rich design capabilities, your paywalls will become most effective and profitable. :::info Please note that the AdaptyUI library is deprecated and is now included as part of AdaptySDK. ::: ## Remove AdaptyUI SDK 1. AdaptyUI becomes a module in Adapty SDK, so please remove `adapty_ui_flutter` from your `pubspec.yaml` file: ```diff showLineNumbers dependencies: + adapty_flutter: ^3.2.1 - adapty_flutter: ^2.10.3 - adapty_ui_flutter: ^2.1.3 ``` 2. Run: ```bash showLineNumbers title="Bash" flutter pub get ``` ## Configure Adapty SDKs Previously, you needed to use `Adapty-Info.plist` and `AndroidManifest.xml` files for Adapty SDK configuration. Now, there's no need to use additional files. Instead, you can provide all required parameters during activation. You only need to configure the Adapty SDK once, typically at the start of your app's lifecycle. ### Activate Adapty module of Adapty SDK 1. Remove the AdaptyUI SDK import from your application as follows: ```diff showLineNumbers import 'package:adapty_flutter/adapty_flutter.dart'; - import 'package:adapty_ui_flutter/adapty_ui_flutter.dart'; ``` 2. Update the Adapty SDK activation like this: ```diff showLineNumbers try { - Adapty().activate(); + await Adapty().activate( + configuration: AdaptyConfiguration(apiKey: 'YOUR_API_KEY') + ..withLogLevel(AdaptyLogLevel.debug) + ..withObserverMode(false) + ..withCustomerUserId(null) + ..withIpAddressCollectionDisabled(false) + ..withIdfaCollectionDisabled(false), + ); } catch (e) { // handle the error } ``` Parameters: | Parameter | Presence | Description | | ----------------------------------- | -------- | ------------------------------------------------------------ | | **PUBLIC_SDK_KEY** | required | The key you can find in the **Public SDK key** field of your app settings in Adapty: [**App settings**-> **General** tab -> **API keys** subsection](https://app.adapty.io/settings/general) | | **withLogLevel** | optional | Adapty logs errors and other crucial information to provide insight into your app's functionality. There are the following available levels:
  • error: Only errors will be logged.
  • warn: Errors and messages from the SDK that do not cause critical errors, but are worth paying attention to will be logged.
  • info: Errors, warnings, and serious information messages, such as those that log the lifecycle of various modules will be logged.
  • verbose: Any additional information that may be useful during debugging, such as function calls, API queries, etc. will be logged.
| | **withObserverMode** | optional |

A boolean value controlling [Observer mode](observer-vs-full-mode). Turn it on if you handle purchases and subscription status yourself and use Adapty for sending subscription events and analytics.

The default value is `false`.

🚧 When running in Observer mode, Adapty SDK won't close any transactions, so make sure you're handling it.

| | **withCustomerUserId** | optional | An identifier of the user in your system. We send it in subscription and analytical events, to attribute events to the right profile. You can also find customers by `customerUserId` in the [**Profiles and Segments**](https://app.adapty.io/profiles/users) menu. | | **withIdfaCollectionDisabled** | optional |

Set to `true` to disable IDFA collection and sharing.

the user IP address sharing.

The default value is `false`.

For more details on IDFA collection, refer to the [Analytics integration](analytics-integration#disable-collection-of-advertising-identifiers) section.

| | **withIpAddressCollectionDisabled** | optional |

Set to `true` to disable user IP address collection and sharing.

The default value is `false`.

| ### Activate AdaptyUI module of Adapty SDK You need to configure the AdaptyUI module only if you plan to use [Paywall Builder](adapty-paywall-builder.md): ```dart showLineNumbers title="Dart" try { final mediaCache = AdaptyUIMediaCacheConfiguration( memoryStorageTotalCostLimit: 100 * 1024 * 1024, // 100MB memoryStorageCountLimit: 2147483647, // 2^31 - 1, max int value in Dart diskStorageSizeLimit: 100 * 1024 * 1024, // 100MB ); await AdaptyUI().activate( configuration: AdaptyUIConfiguration(mediaCache: mediaCache), observer: , ); } catch (e) { // handle the error } ``` Please note that AdaptyUI configuration is optional, you can activate AdaptyUI module without its config. However, if you use the config, all parameters are required in it. Parameters: | Parameter | Presence | Description | | :------------------------------ | :------- | :----------------------------------------------------------- | | **memoryStorageTotalCostLimit** | required | Total cost limit of the storage in bytes. | | **memoryStorageCountLimit** | required | The item count limit of the memory storage. | | **diskStorageSizeLimit** | required | The file size limit on disk of the storage in bytes. 0 means no limit. | --- # File: flutter-legacy-install --- --- title: "Legacy installation guide" description: "Get started with Adapty on Flutter to streamline subscription setup and management." --- Adapty comprises two crucial SDKs for seamless integration into your mobile app: - Core **AdaptySDK**: This is a fundamental, mandatory SDK necessary for the proper functioning of Adapty within your app. - **AdaptyUI SDK**: This optional SDK becomes necessary if you use the Adapty Paywall Builder: a user-friendly, no-code tool for easily creating cross-platform paywalls. These paywalls are built in a visual constructor right in our dashboard, run entirely natively on the device, and require minimal effort from you to create something that performs well. Please consult the compatibility table below to choose the correct pair of Adapty SDK and AdaptyUI SDK. | Adapty SDK version | AdaptyUI SDK version | | :----------------- | :------------------- | | 2.9.3 | 2.1.0 | | 2.10.0 | 2.1.1 | | 2.10.1 | 2.1.2 | | 2.10.3 | 2.1.3 | ## Install Adapty SDK 1. Add the Adapty and AdaptyUI modules to your `pubspec.yaml` file: ```yaml showLineNumbers title="pubspec.yaml" dependencies: adapty_flutter: ^2.10.3 adapty_ui_flutter: ^2.1.3 ``` 2. Run: ```bash showLineNumbers title="Bash" flutter pub get ``` 3. Import Adapty modules in your application in the following way: ```dart showLineNumbers title="Dart" import 'package:adapty_flutter/adapty_flutter.dart'; import 'package:adapty_ui_flutter/adapty_ui_flutter.dart'; ``` ## Configure Adapty SDK The configuration of the Adapty SDK for Flutter slightly differs depending on the mobile operating system (iOS or Android) you are going to release it for. ### Configure Adapty SDK for iOS Create `Adapty-Info.plist` and add it to your project. Add the flag `AdaptyPublicSdkKey` in this file with the value of your Public SDK key. ```xml showLineNumbers title="Adapty-Info.plist" AdaptyPublicSdkKey PUBLIC_SDK_KEY AdaptyObserverMode ``` Parameters: | Parameter | Presence | Description | | ---------------------- | -------- | ------------------------------------------------------------ | | AdaptyPublicSdkKey | required | The key you can find in the **Public SDK key** field of your app settings in Adapty: [**App settings**-> **General** tab -> **API keys** subsection](https://app.adapty.io/settings/general) | | AdaptyObserverMode | optional |

A boolean value controlling [Observer mode](observer-vs-full-mode). Turn it on if you handle purchases and subscription status yourself and use Adapty for sending subscription events and analytics. At any purchase or restore in your application, you'll need to call `.restorePurchases()` method to record the action in Adapty. The default value is `false`.

🚧 When running in Observer mode, Adapty SDK won't close any transactions, so make sure you're handling it.

| | idfaCollectionDisabled | optional |

A boolean parameter, that allows you to disable IDFA collection for your iOS app. The default value is `false`.

For more details, refer to the [Analytics integration](analytics-integration#disable-collection-of-advertising-identifiers) section.

| ### Configure Adapty SDK for Android 1. Add the `AdaptyPublicSdkKey` flag into the app’s `AndroidManifest.xml` \(Android) file with the value of your Public SDK key. ```xml showLineNumbers title="AndroidManifest.xml" ... ``` Required parameters: | Parameter | Presence | Description | | ---------------------------- | -------- | ------------------------------------------------------------ | | PUBLIC_SDK_KEY | required |

Contents of the **Public SDK key** field in the [**App Settings** -> **General** tab](https://app.adapty.io/settings/general) in the Adapty Dashboard. **SDK keys** are unique for every app, so if you have multiple apps make sure you choose the right one.

Make sure you use the **Public SDK key** for Adapty initialization, since the **Secret key** should be used for [server-side API](getting-started-with-server-side-api) only.

| | AdaptyObserverMode | optional |

A boolean value that is controlling [Observer mode](observer-vs-full-mode) . Turn it on if you handle purchases and subscription status yourself and use Adapty for sending subscription events and analytics.

The default value is `false`.

🚧 When running in Observer mode, Adapty SDK won't close any transactions, so make sure you're handling it.

| | AdaptyIDFACollectionDisabled | optional |

A boolean parameter, that allows you to disable IDFA collection for your app. The default value is `false`.

For more details, refer to the [Analytics integration](analytics-integration#disable-collection-of-advertising-identifiers) section.

| 2. In your application, add: ```javascript showLineNumbers title="Flutter" import 'package:adapty_flutter/adapty_flutter.dart'; ``` 3. Activate Adapty SDK with the following code: ```javascript showLineNumbers title="Flutter" try { Adapty().activate(); } on AdaptyError catch (adaptyError) {} } catch (e) {} ``` Please keep in mind that for paywalls and products to be displayed in your mobile application, and for analytics to work, you need to display the paywalls and, if you're using paywalls not created with the Paywall Builder, [handle the purchase process](flutter-making-purchases) within your app. ### Set up the logging system Adapty logs errors and other crucial information to provide insight into your app's functionality. There are the following available levels: | Level | Description | | :------ | :----------------------------------------------------------- | | error | Only errors will be logged. | | warn | Errors and messages from the SDK that do not cause critical errors, but are worth paying attention to will be logged. | | info | Errors, warnings, and serious information messages, such as those that log the lifecycle of various modules will be logged. | | verbose | Any additional information that may be useful during debugging, such as function calls, API queries, etc. will be logged. | You can set `logLevel` in your app before configuring Adapty. ```javascript showLineNumbers title="Flutter" try { await Adapty().setLogLevel(AdaptyLogLevel.verbose); } on AdaptyError catch (adaptyError) { } catch (e) {} ``` --- # File: flutter-get-legacy-pb-paywalls --- --- title: "Fetch legacy Paywall Builder paywalls in Flutter SDK" description: "Retrieve legacy PB paywalls in your Flutter app with Adapty SDK." --- After [you designed the visual part for your paywall](adapty-paywall-builder-legacy) with Paywall Builder in the Adapty Dashboard, you can display it in your Flutter app. The first step in this process is to get the paywall associated with the placement and its view configuration as described below. :::warning This guide is for **legacy Paywall Builder paywalls** only which require SDK v2.x or earlier. The process for fetching paywalls differs for paywalls designed with different versions of Paywall Builder and remote config paywalls. - For fetching **New Paywall Builder paywalls**, check out [Fetch new Paywall Builder paywalls and their configuration](flutter-get-pb-paywalls). - For fetching **Remote config paywalls**, see [Fetch paywalls and products for remote config paywalls](fetch-paywalls-and-products-flutter). :::
Before you start displaying paywalls in your Flutter app (click to expand) 1. [Create your products](create-product) in the Adapty Dashboard. 2. [Create a paywall and incorporate the products into it](create-paywall) in the Adapty Dashboard. 3. [Create placements and incorporate your paywall into it](create-placement) in the Adapty Dashboard. 4. [Install Adapty SDK and AdaptyUI DSK](sdk-installation-flutter) in your Flutter app.
## Fetch paywall designed with Paywall Builder If you've [designed a paywall using the Paywall Builder](adapty-paywall-builder-legacy), you don't need to worry about rendering it in your Flutter app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. Nevertheless, you need to get its ID via the placement, its view configuration, and then present it in your Flutter app. To ensure optimal performance, it's crucial to retrieve the paywall and its [view configuration](#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) as early as possible, allowing sufficient time for images to download before presenting them to the user. To get a paywall, use the `getPaywall` method: ```dart showLineNumbers try { final paywall = await Adapty().getPaywall(id: "YOUR_PLACEMENT_ID", locale: "en"); // the requested paywall } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the [paywall localization](add-paywall-locale-in-adapty-paywall-builder). This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.

| **Don't hardcode product IDs.** The only ID you should hardcode is the placement ID. Paywalls are configured remotely, so the number of products and available offers can change at any time. Your app must handle these changes dynamically—if a paywall returns two products today and three tomorrow, display all of them without code changes. Response parameters: | Parameter | Description | | :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html) object with a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch the view configuration of paywall designed using Paywall Builder After fetching the paywall, check if it includes a `viewConfiguration`, which indicates that it was created using Paywall Builder. This will guide you on how to display the paywall. If the `viewConfiguration` is present, treat it as a Paywall Builder paywall; if not, [handle it as a remote config paywall](present-remote-config-paywalls-flutter). For Flutter, the view configuration is automatically handled when you present the paywall using the `AdaptyUI.showPaywall()` method. --- # File: flutter-present-paywalls-legacy --- --- title: "Present legacy Paywall Builder paywalls in Flutter SDK" description: "Present paywalls in Flutter (Legacy) with Adapty." --- If you've customized a paywall using the Paywall Builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. :::warning This guide is for **legacy Paywall Builder paywalls**, which require Adapty SDK up to version 2.x. The process for presenting paywalls differs for paywalls designed with different versions of Paywall Builder and remote config paywalls. - For presenting **New Paywall Builder paywalls**, check out [Flutter - Present new Paywall Builder paywalls](flutter-present-paywalls). - For presenting **Remote config paywalls**, see [Render paywall designed by remote config](present-remote-config-paywalls). . ::: To display a paywall, use the `view.present()` method on the `view` created by the `createPaywallView` method. Each `view` can only be used once. If you need to display the paywall again, call `createPaywallView` one more to create a new `view` instance. :::warning Reusing the same `view` without recreating it may result in an `AdaptyUIError.viewAlreadyPresented` error. ::: ```typescript showLineNumbers title="Flutter" try { await view.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` --- # File: flutter-handling-events-legacy --- --- title: "Handle paywall events in legacy Flutter SDK" description: "Handle subscription events in Flutter (Legacy) with Adapty’s SDK." --- Paywalls configured with the [Paywall Builder](adapty-paywall-builder) don't need extra code to make and restore purchases. However, they generate some events that your app can respond to. Those events include button presses (close buttons, URLs, product selections, and so on) as well as notifications on purchase-related actions taken on the paywall. Learn how to respond to these events below. :::warning This guide is for **legacy Paywall Builder paywalls** only which require Adapty SDK up to v2.x. For presenting paywalls in Adapty SDK v3.0 or later designed with the new Paywall Builder, see [Flutter - Handle paywall events designed with the new Paywall Builder](flutter-handling-events). ::: To control or monitor processes occurring on the paywall screen within your mobile app, implement the `AdaptyUIObserver` methods and register the observer before presenting any screen: ```javascript showLineNumbers title="Flutter" AdaptyUI().addObserver(this); ``` ### User-generated events #### Actions If a user has performed some action (`close`, `openURL`, `androidSystemBack`, or `custom`, this method will be invoked: ```javascript showLineNumbers title="Flutter" // You have to install url_launcher plugin in order to handle urls: // https://pub.dev/packages/url_launcher void paywallViewDidPerformAction(AdaptyUIView view, AdaptyUIAction action) { switch (action.type) { case AdaptyUIActionType.close: view.dismiss(); break; case AdaptyUIActionType.openUrl: final urlString = action.value; if (urlString != null) { launchUrlString(urlString); } default: break; } } ``` The following action types are supported: - `close` - `openUrl` - `custom` - `androidSystemBack`. Note that at the very least you need to implement the reactions to both `close` and `openURL`. For example, if a user taps the close button, the action `close` will occur and you are supposed to dismiss the paywall. Note that `AdaptyUIAction` has optional value property: look at this in the case of `openUrl` and `custom`. > 💡 Login Action > > If you have configured Login Action in the dashboard, you should implement reaction for `custom` action with value `"login"` #### Product selection If a product is selected for purchase (by a user or by the system), this method will be invoked: ```javascript showLineNumbers title="Flutter" void paywallViewDidSelectProduct(AdaptyUIView view, AdaptyPaywallProduct product) { } ``` #### Started purchase If a user initiates the purchase process, this method will be invoked: ```javascript showLineNumbers title="Flutter" void paywallViewDidStartPurchase(AdaptyUIView view, AdaptyPaywallProduct product) { } ``` #### Canceled purchase If a user initiates the purchase process but manually interrupts it, the function below will be invoked. Basically, this event occurs when the `Adapty.makePurchase()` function completes with the `.paymentCancelled` error: ```javascript showLineNumbers title="Flutter" void paywallViewDidCancelPurchase(AdaptyUIView view, AdaptyPaywallProduct product) { } ``` #### Successful purchase If `Adapty.makePurchase()` succeeds, this method will be invoked: ```javascript showLineNumbers title="Flutter" void paywallViewDidFinishPurchase(AdaptyUIView view, AdaptyPaywallProduct product, AdaptyProfile profile) { } ``` #### Failed purchase If `Adapty.makePurchase()` fails, this method will be invoked: ```javascript showLineNumbers title="Flutter" void paywallViewDidFailPurchase(AdaptyUIView view, AdaptyPaywallProduct product, AdaptyError error) { } ``` #### Successful restore If `Adapty.restorePurchases()` succeeds, this method will be invoked: ```javascript showLineNumbers title="Flutter" void paywallViewDidFinishRestore(AdaptyUIView view, AdaptyProfile profile) { } ``` #### Failed restore If `Adapty.restorePurchases()` fails, this method will be invoked: ```javascript showLineNumbers title="Flutter" void paywallViewDidFailRestore(AdaptyUIView view, AdaptyError error) { } ``` ### Data fetching and rendering #### Product loading errors If you don't pass the product array during the initialization, AdaptyUI will retrieve the necessary objects from the server by itself. If this operation fails, AdaptyUI will report the error by invoking this method: ```javascript showLineNumbers title="Flutter" void paywallViewDidFailLoadingProducts(AdaptyUIView view, AdaptyIOSProductsFetchPolicy? fetchPolicy, AdaptyError error) { } ``` #### Rendering errors If an error occurs during the interface rendering, it will be reported by calling this method: ```javascript showLineNumbers title="Flutter" void paywallViewDidFailRendering(AdaptyUIView view, AdaptyError error) { } ``` In a normal situation, such errors should not occur, so if you come across one, please let us know. --- # File: flutter-hide-legacy-paywall-builder-paywalls --- --- title: "Hide legacy Paywall Builder paywalls in Flutter SDK" description: "Hide legacy paywalls in your Flutter app with Adapty SDK." --- While Paywall Builder seamlessly handles the purchasing process upon clicking "buy" buttons, you have to manage the closure of paywall screens within your Flutter app. :::warning This guide covers only hiding **legacy Paywall Builder paywalls** which supports Adapty SDK v2.x or earlier. ::: You can hide a paywall screen by calling the `view.dismiss` method. ```dart showLineNumbers try { await view.dismiss(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` --- # End of Documentation _Generated on: 2026-03-05T16:27:48.069Z_ _Successfully processed: 42/42 files_ # IOS - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Generated on: 2026-03-05T16:27:48.071Z Total files: 43 --- # File: sdk-installation-ios --- --- title: "Install & configure iOS SDK" description: "Step-by-step guide on installing Adapty SDK on iOS for subscription-based apps." --- Adapty SDK includes two key modules for seamless integration into your mobile app: - **Core Adapty**: This essential SDK is required for Adapty to function properly in your app. - **AdaptyUI**: This optional module is needed if you use the [Adapty Paywall Builder](adapty-paywall-builder), a user-friendly, no-code tool for easily creating cross-platform paywalls. :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](https://github.com/adaptyteam/AdaptySDK-iOS/tree/master/Examples), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: For a complete implementation walkthrough, you can also see the videos:
## Requirements While the SDK technically supports iOS 13.0+ for the core module, iOS 15.0+ is effectively required for practical use since: - All StoreKit 2 features require iOS 15.0+ - AdaptyUI module is iOS 15.0+ only ## Install Adapty SDK [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-iOS.svg?style=flat&logo=apple)](https://github.com/adaptyteam/AdaptySDK-iOS/releases) In Xcode, go to **File** -> **Add Package Dependency...**. Note that the steps to add package dependencies may vary between Xcode versions, so refer to Xcode documentation if needed. 1. Enter the repository URL: ``` https://github.com/adaptyteam/AdaptySDK-iOS.git ``` 2. Select the version (latest stable version is recommended) and click **Add Package**. 3. In the **Choose Package Products** window, select the modules you need: - **Adapty** (core module) - **AdaptyUI** (optional - only if you plan to use Paywall Builder) :::note Note: - To enable the [Kids mode](kids-mode.md), select **Adapty_KidsMode** instead of **Adapty**. - Don't select any other packages from the list – you won't need them. ::: 4. Click **Add Package** to complete the installation. 5. **Verify installation:** In your project navigator, you should see "Adapty" (and "AdaptyUI" if selected) under **Package Dependencies**. :::info CocoaPods is now in maintenance mode, with development officially stopped. We recommend switching to [Swift Package Manager](sdk-installation-ios#install-adapty-sdk-via-swift-package-manager). ::: 1. Add Adapty to your `Podfile`. Choose the modules you need: 1. **Adapty** is the mandatory module. 2. **AdaptyUI** is an optional module you need if you plan to use the [Adapty Paywall Builder](adapty-paywall-builder). ```shell showLineNumbers title="Podfile" pod 'Adapty' pod 'AdaptyUI' # optional module needed only for Paywall Builder ``` 2. Run: ```sh showLineNumbers title="Shell" pod install ``` This will create a `.xcworkspace` file for your app. Use this file for all future development. ## Activate Adapty module of Adapty SDK Activate the Adapty SDK in your app code. :::note The Adapty SDK only needs to be activated once in your app. ::: To get your **Public SDK Key**: 1. Go to Adapty Dashboard and navigate to [**App settings → General**](https://app.adapty.io/settings/general). 2. From the **Api keys** section, copy the **Public SDK Key** (NOT the Secret Key). 3. Replace `"YOUR_PUBLIC_SDK_KEY"` in the code. :::important - Make sure you use the **Public SDK key** for Adapty initialization, the **Secret key** should be used for [server-side API](getting-started-with-server-side-api) only. - **SDK keys** are unique for every app, so if you have multiple apps make sure you choose the right one. ::: ```swift showLineNumbers @main struct YourApp: App { init() { // Configure Adapty SDK let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") // Get from Adapty dashboard Adapty.logLevel = .verbose // recommended for development and the first production release let config = configurationBuilder.build() // Activate Adapty SDK asynchronously Task { do { try await Adapty.activate(with: config) } catch { // Handle error appropriately for your app print("Adapty activation failed: ", error) } } var body: some Scene { WindowGroup { // Your content view } } } } ``` ```swift showLineNumbers // In your AppDelegate class: // If you only use an AppDelegate, place the following code in the // application(_:didFinishLaunchingWithOptions:) method. // If you use a SceneDelegate, place the following code in the // scene(_:willConnectTo:options:) method. Task { do { let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") // Get from Adapty dashboard .with(logLevel: .verbose) // recommended for development and the first production release let config = configurationBuilder.build() try await Adapty.activate(with: config) } catch { // Handle error appropriately for your app print("Adapty activation failed: ", error) } } ``` Now set up paywalls in your app: - If you use [Adapty Paywall Builder](adapty-paywall-builder), first [activate the AdaptyUI module](#activate-adaptyui-module-of-adapty-sdk) below, then follow the [Paywall Builder quickstart](ios-quickstart-paywalls). - If you build your own paywall UI, see the [quickstart for custom paywalls](ios-quickstart-manual). ## Activate AdaptyUI module of Adapty SDK If you plan to use [Paywall Builder](adapty-paywall-builder.md) and have [installed AdaptyUI module](sdk-installation-ios#install-sdks-via-cocoapods), you also need to activate AdaptyUI. :::important In your code, you must activate the core Adapty module before activating AdaptyUI. ::: ```swift showLineNumbers title="Swift" @main struct YourApp: App { init() { // ...ConfigurationBuilder steps // Activate Adapty SDK asynchronously Task { do { try await Adapty.activate(with: config) try await AdaptyUI.activate() } catch { // Handle error appropriately for your app print("Adapty activation failed: ", error) } } // main body... } } ``` ```swift showLineNumbers title="UIKit" // If you only use an AppDelegate, place the following code in the // application(_:didFinishLaunchingWithOptions:) method. // If you use a SceneDelegate, place the following code in the // scene(_:willConnectTo:options:) method. Task { do { let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") // Get from Adapty dashboard .with(logLevel: .verbose) // recommended for development let config = configurationBuilder.build() try await Adapty.activate(with: config) try await AdaptyUI.activate() } catch { // Handle error appropriately for your app print("Adapty activation failed: ", error) } } ``` :::tip Optionally, when activating AdaptyUI, you can [override default caching settings for paywalls](#set-up-media-cache-configuration-for-adaptyui). ::: ## Optional setup ### Logging #### Set up the logging system Adapty logs errors and other important information to help you understand what is going on. There are the following levels available: | Level | Description | | ---------- | ------------------------------------------------------------ | | `error` | Only errors will be logged | | `warn` | Errors and messages from the SDK that do not cause critical errors, but are worth paying attention to will be logged | | `info` | Errors, warnings, and various information messages will be logged | | `verbose` | Any additional information that may be useful during debugging, such as function calls, API queries, etc. will be logged | ```swift showLineNumbers let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") .with(logLevel: .verbose) // recommended for development ``` #### Redirect the logging system messages If you need to send Adapty's log messages to your system or save them to a file, use the `setLogHandler` method and implement your custom logging logic inside it. This handler receives log records containing message content and severity level. ```swift showLineNumbers title="Swift" Adapty.setLogHandler { record in writeToLocalFile("Adapty \(record.level): \(record.message)") } ``` ### Data policies Adapty doesn't store personal data of your users unless you explicitly send it, but you can implement additional data security policies to comply with the store or country guidelines. #### Disable IDFA collection and sharing When activating the Adapty module, set `idfaCollectionDisabled` to `true` to disable IDFA collection and sharing. Use this parameter to comply with App Store Review Guidelines or avoid triggering the App Tracking Transparency prompt when IDFA isn't needed for your app. The default value is `false`. For more details on IDFA collection, refer to the [Analytics integration](analytics-integration#disable-collection-of-advertising-identifiers) section. ```swift showLineNumbers let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") .with(idfaCollectionDisabled: true) ``` #### Disable IP collection and sharing When activating the Adapty module, set `ipAddressCollectionDisabled` to `true` to disable user IP address collection and sharing. The default value is `false` Use this parameter to enhance user privacy, comply with regional data protection regulations (like GDPR or CCPA), or reduce unnecessary data collection when IP-based features aren't required for your app. ```swift showLineNumbers let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") .with(ipAddressCollectionDisabled: true) ``` #### Media cache configuration for paywalls in AdaptyUI Please note that the AdaptyUI configuration is optional. You can activate the AdaptyUI module without its config. However, if you use the config, all parameters are required. ```swift showLineNumbers title="Swift" // Configure AdaptyUI let adaptyUIConfiguration = AdaptyUI.Configuration( mediaCacheConfiguration: .init( memoryStorageTotalCostLimit: 100 * 1024 * 1024, memoryStorageCountLimit: .max, diskStorageSizeLimit: 100 * 1024 * 1024 ) ) // Activate AdaptyUI AdaptyUI.activate(configuration: adaptyUIConfiguration) ``` Parameters: | Parameter | Presence | Description | | :-------------------------- | :------- | :----------------------------------------------------------- | | memoryStorageTotalCostLimit | required | Total cost limit of the storage in bytes. | | memoryStorageCountLimit | required | The item count limit of the memory storage. | | diskStorageSizeLimit | required | The file size limit on disk of the storage in bytes. 0 means no limit. | ### Transaction finishing behavior :::info This feature is available starting from SDK version 3.12.0. ::: By default, Adapty automatically finishes transactions after successful validation. However, if you need advanced transaction validation (such as server-side receipt validation, fraud detection, or custom business logic), you can configure the SDK to use manual transaction finishing. ```swift showLineNumbers title="Swift" let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") .with(transactionsFinishBehavior: .manual) // .auto is the default ``` See more details on how to finish transactions in the [guide](ios-transaction-management). ### Clear data on backup restore When `clearDataOnBackup` is set to `true`, the SDK detects when the app is restored from an iCloud backup and deletes all locally stored SDK data, including cached profile information, product details, and paywalls. The SDK then initializes with a clean state. Default value is `false`. :::note Only local SDK cache is deleted. Transaction history with Apple and user data on Adapty servers remain unchanged. ::: ```swift showLineNumbers let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") .with(clearDataOnBackup: true) // default – false ``` ## Troubleshooting #### Swift 6 concurrency error with Tuist When building with [Tuist](https://tuist.dev/), you may see Swift 6 strict concurrency compilation errors. Typical symptoms include `@Sendable` attribute mismatches in `AdaptyUIBuilderLogic` or similar cross-module Sendability errors. This happens because Tuist generates Xcode projects from SPM packages but doesn't preserve the `swift-tools-version: 6.0` setting. As a result, some Adapty targets (`Adapty`, `AdaptyUI`, `AdaptyUIBuilder`) compile with Swift 5 rules while others use Swift 6, creating cross-module `@Sendable` mismatches. **Fix**: Upgrade to Adapty SDK **3.15.5** or later, which resolves the issue regardless of mixed Swift language versions. **Workaround**: If you can't upgrade, explicitly set Swift 6 for all three Adapty targets in your Tuist configuration: ```swift showLineNumbers targetSettings: [ "Adapty": .init().swiftVersion("6"), "AdaptyUI": .init().swiftVersion("6"), "AdaptyUIBuilder": .init().swiftVersion("6"), ] ``` --- # File: ios-quickstart-paywalls --- --- title: "Enable purchases by using paywalls in iOS SDK" description: "Quickstart guide to setting up Adapty for in-app subscription management." --- To enable in-app purchases, you need to understand three key concepts: - [**Products**](product.md) – anything users can buy (subscriptions, consumables, lifetime access) - [**Paywalls**](paywalls.md) are configurations that define which products to offer. In Adapty, paywalls are the only way to retrieve products, but this design lets you modify offerings, pricing, and product combinations without touching your app code. - [**Placements**](placements.md) – where and when you show paywalls in your app (like `main`, `onboarding`, `settings`). You set up paywalls for placements in the dashboard, then request them by placement ID in your code. This makes it easy to run A/B tests and show different paywalls to different users. Adapty offers you three ways to enable purchases in your app. Select one of them depending on your app requirements: | Implementation | Complexity | When to use | |------------------------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Adapty Paywall Builder | ✅ Easy | You [create a complete, purchase-ready paywall in the no-code builder](quickstart-paywalls). Adapty automatically renders it and handles all the complex purchase flow, receipt validation, and subscription management behind the scenes. | | Manually created paywalls | 🟡 Medium | You implement your paywall UI in your app code, but still get the paywall object from Adapty to maintain flexibility in product offerings. See the [guide](ios-quickstart-manual). | | Observer mode | 🔴 Hard | You already have your own purchase handling infrastructure and want to keep using it. Note that the observer mode has its limitations in Adapty. See the [article](observer-vs-full-mode). | :::important **The steps below show how to implement a paywall created in the Adapty paywall builder.** If you don't want to use the paywall builder, see the [guide for handling purchases in manually created paywalls](making-purchases.md). ::: To display a paywall created in the Adapty paywall builder, in your app code, you only need to: 1. **Get the paywall**: Get the paywall from Adapty. 2. **Display the paywall and Adapty will handle purchases for you**: Show the paywall container you've got in your app. 3. **Handle button actions**: Associate user interactions with the paywall with your app's response to them. For example, open links or close the paywall when users click buttons. ## Before you start Before you start, complete these steps: 1. [Connect your app to the App Store](initial_ios) in the Adapty Dashboard. 2. [Create your products](create-product) in Adapty. 3. [Create a paywall and add products to it](create-paywall). 4. [Create a placement and add your paywall to it](create-placement). 5. [Install and activate the Adapty SDK](sdk-installation-ios) in your app code. :::tip The fastest way to complete these steps is to follow the [quickstart guide](quickstart). ::: ## 1. Get the paywall created in the paywall builder Your paywalls are associated with placements configured in the dashboard. Placements allow you to run different paywalls for different audiences or to run [A/B tests](ab-tests.md). To get a paywall created in the Adapty paywall builder, you need to: 1. Get the `paywall` object by the [placement](placements.md) ID using the `getPaywall` method and check whether it is a paywall created in the builder. 2. Get the paywall view configuration using the `getPaywallConfiguration` method. The view configuration contains the UI elements and styling needed to display the paywall. :::important To get the view configuration, you must switch on the **Show on device** toggle in the Paywall Builder. Otherwise, you will get an empty view configuration, and the paywall won't be displayed. ::: ```swift func loadPaywall() async { let paywall = try await Adapty.getPaywall("YOUR_PLACEMENT_ID") guard paywall.hasViewConfiguration else { print("Paywall doesn't have view configuration") return } paywallConfiguration = try await AdaptyUI.getPaywallConfiguration(forPaywall: paywall) } ``` ## 2. Display the paywall Now, when you have the paywall configuration, it's enough to add a few lines to display your paywall. In SwiftUI, when displaying the paywall, you also need to handle events. Some of them are optional, but `didFailPurchase`, `didFinishRestore`, `didFailRestore`, and `didFailRendering` are required. When testing, you can just copy the code from the snippet below to log these errors. :::tip Handling `didFinishPurchase` isn't required, but is useful when you want to perform actions after a successful purchase. If you don't implement that callback, the paywall will dismiss automatically. ::: ```swift .paywall( isPresented: $paywallPresented, paywallConfiguration: paywallConfiguration, didFailPurchase: { product, error in print("Purchase failed: \(error)") }, didFinishRestore: { profile in print("Restore finished successfully") }, didFailRestore: { error in print("Restore failed: \(error)") }, didFailRendering: { error in paywallPresented = false print("Rendering failed: \(error)") }, showAlertItem: $alertItem ) ``` ```swift func presentPaywall(with config: AdaptyUI.PaywallConfiguration) { let paywallController = AdaptyUI.paywallController( with: config, delegate: self ) present(paywallController, animated: true) } ``` :::info For more details on how to display a paywall, see our [guide](ios-present-paywalls.md). ::: ## 3. Handle button actions When users click buttons in the paywall, the iOS SDK automatically handles purchases, restoration, closing the paywall, and opening links. However, other buttons have custom or pre-defined IDs and require handling actions in your code. Or, you may want to override their default behavior. For example, here is the default behavior for the close button. You don't need to add it in the code, but here, you can see how it is done if needed. :::tip Read our guides on how to handle button [actions](handle-paywall-actions.md) and [events](ios-handling-events.md). ::: ```swift didPerformAction: { action in switch action { case let .close: paywallPresented = false // default behavior default: break } } ``` ```swift func paywallController(_ controller: AdaptyPaywallController, didPerform action: AdaptyUI.Action) { switch action { case let .close: controller.dismiss(animated: true) // default behavior break } } ``` ## Next steps Your paywall is ready to be displayed in the app. [Test your purchases in sandbox mode](test-purchases-in-sandbox) to make sure you can complete a test purchase from the paywall. Now, you need to [check the users' access level](ios-check-subscription-status.md) to ensure you display a paywall or give access to paid features to right users. ## Full example Here is how all the steps from this guide can be integrated in your app together. ```swift struct ContentView: View { @State private var paywallPresented = false @State private var alertItem: AlertItem? @State private var paywallConfiguration: AdaptyUI.PaywallConfiguration? @State private var isLoading = false @State private var hasInitialized = false var body: some View { VStack { if isLoading { ProgressView("Loading...") } else { Text("Your App Content") } } .task { guard !hasInitialized else { return } await initializePaywall() hasInitialized = true } .paywall( isPresented: $paywallPresented, configuration: paywallConfiguration, didPerformAction: { action in switch action.type { case let .close: paywallPresented = false default: break } }, didFailPurchase: { product, error in print("Purchase failed: \(error)") }, didFinishRestore: { profile in print("Restore finished successfully") }, didFailRestore: { error in print("Restore failed: \(error)") }, didFailRendering: { error in print("Rendering failed: \(error)") }, showAlertItem: $alertItem ) } private func initializePaywall() async { isLoading = true defer { isLoading = false } await loadPaywall() paywallPresented = true } } private func loadPaywall() async { do { let paywall = try await Adapty.getPaywall("YOUR_PLACEMENT_ID") guard paywall.hasViewConfiguration else { print("Paywall doesn't have view configuration") return } paywallConfiguration = try await AdaptyUI.getPaywallConfiguration(forPaywall: paywall) } catch { print("Failed to load paywall: \(error)") } } } ``` ```swift class ViewController: UIViewController { private var paywallConfiguration: AdaptyUI.PaywallConfiguration? override func viewDidLoad() { super.viewDidLoad() Task { await initializePaywall() } } private func initializePaywall() async { do { paywallConfiguration = try await loadPaywall() if let paywallConfiguration { await MainActor.run { presentPaywall(with: paywallConfiguration) } } } catch { print("Error initializing paywall: \(error)") } } private func loadPaywall() async throws -> AdaptyUI.PaywallConfiguration? { let paywall = try await Adapty.getPaywall("YOUR_PLACEMENT_ID") guard paywall.hasViewConfiguration else { print("Paywall doesn't have view configuration") return nil } return try await AdaptyUI.getPaywallConfiguration(forPaywall: paywall) } private func presentPaywall(with config: AdaptyUI.PaywallConfiguration) { let paywallController = AdaptyUI.paywallController(with: config, delegate: self) present(paywallController, animated: true) } } // MARK: - AdaptyPaywallControllerDelegate extension ViewController: AdaptyPaywallControllerDelegate { func paywallController(_ controller: AdaptyPaywallController, didPerform action: AdaptyUI.Action) { switch action { case let .close: controller.dismiss(animated: true) break } } func paywallController(_ controller: AdaptyUI.PaywallController, didFailPurchase product: AdaptyPaywallProduct, error: AdaptyError) { print("Purchase failed for \(product.vendorProductId): \(error)") guard error.adaptyErrorCode != .paymentCancelled else { return // Don't show alert for user cancellation } let message = switch error.adaptyErrorCode { case .paymentNotAllowed: "Purchases are not allowed on this device." default: "Purchase failed. Please try again." } let alert = UIAlertController(title: "Purchase Error", message: message, preferredStyle: .alert) let okAction = UIAlertAction(title: "OK", style: .default) { _ in } alert.addAction(okAction) present(alert, animated: true) } func paywallController(_ controller: AdaptyUI.PaywallController, didFinishRestore profile: AdaptyProfile) { print("Restore finished successfully") controller.dismiss(animated: true) } func paywallController(_ controller: AdaptyUI.PaywallController, didFailRestore error: AdaptyError) { print("Restore failed: \(error)") } func paywallController(_ controller: AdaptyUI.PaywallController, didFailRendering error: AdaptyError) { print("Rendering failed: \(error)") controller.dismiss(animated: true) } } ``` --- # File: ios-check-subscription-status --- --- title: "Check subscription status in iOS SDK" description: "Learn how to check subscription status in your iOS app with Adapty." --- To decide whether users can access paid content or see a paywall, you need to check their [access level](access-level.md) in the profile. This article shows you how to access the profile state to decide what users need to see - whether to show them a paywall or grant access to paid features. ## Get subscription status When you decide whether to show a paywall or paid content to a user, you check their [access level](access-level.md) in their profile. You have two options: - Call `getProfile` if you need the latest profile data immediately (like on app launch) or want to force an update. - Set up **automatic profile updates** to keep a local copy that's automatically refreshed whenever the subscription status changes. :::important By default, the `premium` access level already exists in Adapty. If you don't need to set up more than one access level, you can just use `premium`. ::: ### Get profile The easiest way to get the subscription status is to use the `getProfile` method to access the profile: ```swift showLineNumbers do { let profile = try await Adapty.getProfile() if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // grant access to premium features } } catch { // handle the error } ``` ```swift showLineNumbers Adapty.getProfile { result in if let profile = try? result.get() { // check the access profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // grant access to premium features } } } ``` ### Listen to subscription updates If you want to automatically receive profile updates in your app: 1. Conform to the `AdaptyDelegate` protocol in a type of your choice and implement the `didLoadLatestProfile` method - Adapty will automatically call this method whenever the user's subscription status changes. In the example below we use a `SubscriptionManager` type to assist with handling subscription workflows and the user's profile. This type can be injected as a dependency or set up as a singleton in a UIKit app, or added to the SwiftUI environment from the app main struct. 2. Store the updated profile data when this method is called, so you can use it throughout your app without making additional network requests. ```swift class SubscriptionManager: AdaptyDelegate { nonisolated func didLoadLatestProfile(_ profile: AdaptyProfile) { let hasAccess = profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false // Update UI, unlock content, etc. } } // Set delegate after Adapty activation Adapty.delegate = subscriptionManager ``` :::note Adapty automatically calls `didLoadLatestProfile` when your app starts, providing cached subscription data even if the device is offline. ::: ## Connect profile with paywall logic When you need to make immediate decisions about showing paywalls or granting access to paid features, you can check the user's profile directly. This approach is useful for scenarios like app launch, when entering premium sections, or before displaying specific content. ```swift private func checkAccessLevel() async -> Bool { do { let profile = try await Adapty.getProfile() return profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false } catch { print("Error checking access level: \(error)") return false } } // In your initialization logic: let hasAccess = await checkAccessLevel() if !hasAccess { paywallPresented = true // Show paywall if no access } ``` ```swift private func checkAccessLevel() async throws -> Bool { let profile = try await Adapty.getProfile() return profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false } // In your initialization logic: let hasAccess = try await checkAccessLevel() if !hasAccess { presentPaywall(with: paywallConfiguration) } ``` ## Next steps Now, when you know how to track the subscription status, [learn how to work with user profiles](ios-quickstart-identify.md) to ensure it aligns with your existing authentication system and paid access sharing permissions. If you don't have your own authentication system, that's not a problem at all, and Adapty will manage users for you, but you can still read the [guide](ios-quickstart-identify.md) to learn how Adapty works with anonymous users. --- # File: ios-quickstart-identify --- --- title: "Identify users in iOS SDK" description: "Quickstart guide to setting up Adapty for in-app subscription management." --- :::important This guide is for you if you have your own authentication system. Here, you will learn how to work with user profiles in Adapty to ensure it aligns with your existing authentication system. ::: How you manage users' purchases depends on your app's authentication model: - If your app doesn't use backend authentication and doesn't store user data, see the [section about anonymous users](#anonymous-users). - If your app has (or will have) backend authentication, see the [section about identified users](#identified-users). **Key concepts**: - **Profiles** are the entities required for the SDK to work. Adapty creates them automatically. - They can be anonymous **(without customer user ID)** or identified **(with customer user ID)**. - You provide **customer user ID** in order to cross-reference profiles in Adapty with your internal auth system Here is what is different for anonymous and identified users: | | Anonymous users | Identified users | |-------------------------|---------------------------------------------------|-------------------------------------------------------------------------| | **Purchase management** | Store-level purchase restoration | Maintain purchase history across devices through their customer user ID | | **Profile management** | New profiles on each reinstall | The same profile across sessions and devices | | **Data persistence** | Anonymous users' data is tied to app installation | Identified users' data persists across app installations | ## Anonymous users If you don't have backend authentication, **you don't need to handle authentication in the app code**: 1. When the SDK is activated on the app's first launch, Adapty **creates a new profile for the user**. 2. When the user purchases anything in the app, this purchase is **associated with their Adapty profile and their store account**. 3. When the user **re-installs** the app or installs it from a **new device**, Adapty **creates a new anonymous profile on activation**. 4. If the user has previously made purchases in your app, by default, their purchases are automatically synced from the App Store on the SDK activation. :::note Backup restores behave differently from reinstalls. By default, when a user restores from a backup, the SDK preserves cached data and does not create a new profile. You can configure this behavior using the `clearDataOnBackup` setting. [Learn more](sdk-installation-ios#clear-data-on-backup-restore). ::: So, with anonymous users, new profiles will be created on each installation, but that's not a problem because, in the Adapty analytics, you can [configure what will be considered a new installation](general#4-installs-definition-for-analytics). For anonymous users, you need to count installs by **device IDs**. In this case, each app installation on a device is counted as an install, including reinstalls. ## Identified users You have two options to identify users in the app: - [**During login/signup:**](#during-loginsignup) If users sign in after your app starts, call `identify()` with a customer user ID when they authenticate. - [**During the SDK activation:**](#during-the-sdk-activation) If you already have a customer user ID stored when the app launches, send it when calling `activate()`. :::important By default, when Adapty receives a purchase from a Customer User ID that is currently associated with another Customer User ID, the access level is shared, so both profiles have paid access. You can configure this setting to transfer paid access from one profile to another or disable sharing completely. See the [article](general#6-sharing-purchases-between-user-accounts) for more details. ::: ### During login/signup If you're identifying users after the app launch (for example, after they log into your app or sign up), use the `identify` method to set their customer user ID. - If you **haven't used this customer user ID before**, Adapty will automatically link it to the current profile. - If you **have used this customer user ID to identify the user before**, Adapty will switch to working with the profile associated with this customer user ID. :::important Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one. ::: ```swift showLineNumbers do { try await Adapty.identify("YOUR_USER_ID") // Unique for each user } catch { // handle the error } ``` ```swift showLineNumbers // User IDs must be unique for each user Adapty.identify("YOUR_USER_ID") { error in if let error { // handle the error } } ``` ### During the SDK activation If you already know a customer user ID when you activate the SDK, you can send it in the `activate` method instead of calling `identify` separately. If you know a customer user ID but set it only after the activation, that will mean that, upon activation, Adapty will create a new anonymous profile and switch to the existing one only after you call `identify`. You can pass either an existing customer user ID (the one you have used before) or a new one. If you pass a new one, a new profile created upon activation will be automatically linked to the customer user ID. :::note By default, creating anonymous profiles does not affect analytics dashboards, because installs are counted based on device IDs. A device ID represents a single installation of the app from the store on a device and is regenerated only after the app is reinstalled. It does not depend on whether this is a first or repeated installation, or whether an existing customer user ID is used. Creating a profile (on SDK activation or logout), logging in, or upgrading the app without reinstalling the app does not generate additional install events. If you want to count installs based on unique users rather than devices, go to **App settings** and configure [**Installs definition for analytics**](general#4-installs-definition-for-analytics). ::: ```swift showLineNumbers // Place in the app main struct for SwiftUI or in AppDelegate for UIKit let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(customerUserId: "YOUR_USER_ID") // Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one. do { try await Adapty.activate(with: configurationBuilder.build()) } catch { // handle the error } ``` ```swift showLineNumbers // Place in the app main struct for SwiftUI or in AppDelegate for UIKit let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(customerUserId: "YOUR_USER_ID") // Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one. Adapty.activate(with: configurationBuilder.build()) { error in // handle the error } ``` ### Log users out If you have a button for logging users out, use the `logout` method. :::important Logging users out creates a new anonymous profile for the user. ::: ```swift showLineNumbers do { try await Adapty.logout() } catch { // handle the error } ``` ```swift showLineNumbers Adapty.logout { error in if error == nil { // successful logout } } ``` :::info To log users back into the app, use the `identify` method. ::: ### Allow purchases without login If your users can make purchases both before and after they log into your app, you need to ensure that they will keep access after they log in: 1. When a logged-out user makes a purchase, Adapty ties it to their anonymous profile ID. 2. When the user logs into their account, Adapty switches to working with their identified profile. - If it is a new customer user ID (e.g., the purchase has been made before registration), Adapty assigns the customer user ID to the current profile, so all the purchase history is maintained. - If it is an existing customer user ID (the customer user ID is already linked to a profile), you need to get the actual access level after the profile switch. You can either call [`getProfile`](ios-check-subscription-status.md) right after the identification, or [listen for profile updates](ios-check-subscription-status.md) so the data syncs automatically. ## Next steps Congratulations! You have implemented in-app payment logic in your app! We wish you all the best with your app monetization! To get even more from Adapty, you can explore these topics: - [**Testing**](test-purchases-in-sandbox.md): Ensure that everything works as expected - [**Onboardings**](ios-onboardings.md): Engage users with onboardings and drive retention - [**Integrations**](configuration.md): Integrate with marketing attribution and analytics services in just one line of code - [**Set custom profile attributes**](setting-user-attributes.md): Add custom attributes to user profiles and create segments, so you can launch A/B tests or show different paywalls to different users --- # File: adapty-cursor --- --- title: "Integrate Adapty into your iOS app with AI assistance" description: "A step-by-step guide to integrating Adapty into your iOS app using Cursor, Context7, ChatGPT, Claude, or other AI tools." --- This guide helps you integrate Adapty into your iOS app with the help of an LLM. You'll start by preparing your Adapty dashboard, then work through each implementation stage by sending focused doc links to your LLM. At the end, you'll find best practices for setting up your AI tools with Adapty documentation. :::tip Copy this entire page as Markdown and paste it into your LLM to get started — click **Copy for LLM** at the top of the page or open [the .md version](https://adapty.io/docs/adapty-cursor.md). The LLM will use the guide links and checkpoints to walk you through each stage. ::: ## Before you start: dashboard checklist Adapty requires dashboard configuration before you write any SDK code. Your LLM cannot look up dashboard values for you — you'll need to provide them. ### Required before coding 1. **Connect your app store**: In the Adapty Dashboard, go to **App settings → General**. This is required for purchases to work. [Connect App Store](integrate-payments.md) 2. **Copy your Public SDK key**: In the Adapty Dashboard, go to **App settings → General**, then find the **API keys** section. In code, this is the string you pass to `Adapty.activate("YOUR_PUBLIC_SDK_KEY")`. 3. **Create at least one product**: In the Adapty Dashboard, go to the **Products** page. You don't reference products directly in code — Adapty delivers them through paywalls. [Add products](quickstart-products.md) 4. **Create a paywall and a placement**: In the Adapty Dashboard, create a paywall on the **Paywalls** page, then assign it to a placement on the **Placements** page. In code, the placement ID is the string you pass to `Adapty.getPaywall("YOUR_PLACEMENT_ID")`. [Create paywall](quickstart-paywalls.md) 5. **Set up access levels**: In the Adapty Dashboard, configure per product on the **Products** page. In code, the string checked in `profile.accessLevels["premium"]`. The default `premium` access level works for most apps. If paying users get access to different features depending on the product (for example, a `basic` plan vs. a `pro` plan), [create additional access levels](assigning-access-level-to-a-product.md) before you start coding. :::tip Once you have all five, you're ready to write code. Tell your LLM: "My Public SDK key is X, my placement ID is Y" so it can generate correct initialization and paywall-fetching code. ::: ### Set up when ready These are not required to start coding, but you'll want them as your integration matures: - **A/B tests**: Configure on the **Placements** page. No code change needed. [A/B tests](ab-tests.md) - **Additional paywalls and placements**: Add more `getPaywall` calls with different placement IDs. - **Analytics integrations**: Configure on the **Integrations** page. Setup varies by integration. See [analytics integrations](analytics-integration.md) and [attribution integrations](attribution-integration.md). ## Feed Adapty docs to your LLM ### Use Context7 (recommended) [Context7](https://context7.com) is an MCP server that gives your LLM direct access to up-to-date Adapty documentation. Your LLM fetches the right docs automatically based on what you ask — no manual URL pasting needed. Context7 works with **Cursor**, **Claude Code**, **Windsurf**, and other MCP-compatible tools. To set it up, run: ``` npx ctx7 setup ``` This detects your editor and configures the Context7 server. For manual setup, see the [Context7 GitHub repository](https://github.com/upstash/context7). Once configured, reference the Adapty library in your prompts: ``` Use the adaptyteam/adapty-docs library to look up how to install the iOS SDK ``` :::warning Even though Context7 removes the need to paste doc links manually, the implementation order matters. Follow the [implementation walkthrough](#implementation-walkthrough) below step by step to make sure everything works. ::: ### Use plain text docs You can access any Adapty doc as plain text Markdown. Add `.md` to the end of its URL, or click **Copy for LLM** under the article title. For example: [adapty-cursor.md](https://adapty.io/docs/adapty-cursor.md). Each stage in the [implementation walkthrough](#implementation-walkthrough) below includes a "Send this to your LLM" block with `.md` links to paste. For more documentation at once, see [index files and platform-specific subsets](#plain-text-doc-index-files) below. ## Implementation walkthrough The rest of this guide walks through Adapty integration in implementation order. Each stage includes the docs to send to your LLM, what you should see when done, and common issues. ### Plan your integration Before jumping into code, ask your LLM to analyze your project and create an implementation plan. If your AI tool supports a planning mode (like Cursor's or Claude Code's plan mode), use it so the LLM can read both your project structure and the Adapty docs before writing any code. Tell your LLM which approach you use for purchases — this affects the guides it should follow: - [**Adapty Paywall Builder**](adapty-paywall-builder.md): You create paywalls in Adapty's no-code builder, and the SDK renders them automatically. - [**Manually created paywalls**](making-purchases.md): You build your own paywall UI in code but still use Adapty to fetch products and handle purchases. - [**Observer mode**](observer-vs-full-mode.md): You keep your existing purchase infrastructure and use Adapty only for analytics and integrations. Not sure which one to pick? Read the [comparison table in the quickstart](ios-quickstart-paywalls.md). ### Install and configure the SDK Install the Adapty SDK package via Swift Package Manager in Xcode and activate it with your Public SDK key. This is the foundation — nothing else works without it. **Guide:** [Install & configure Adapty SDK](sdk-installation-ios.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/sdk-installation-ios.md ``` :::tip[Checkpoint] - **Expected:** App builds and runs. Xcode console shows Adapty activation log. - **Gotcha:** "Public API key is missing" → check you replaced the placeholder with your real key from App settings. ::: ### Show paywalls and handle purchases Fetch a paywall by placement ID, display it, and handle purchase events. The guides you need depend on how you handle purchases. Test each purchase in the sandbox as you go — don't wait until the end. See [Test purchases in sandbox](test-purchases-in-sandbox.md) for setup instructions. **Guides:** - [Enable purchases using paywalls (quickstart)](ios-quickstart-paywalls.md) - [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls.md) - [Display paywalls](ios-present-paywalls.md) - [Handle paywall events](ios-handling-events.md) - [Respond to button actions](handle-paywall-actions.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/ios-quickstart-paywalls.md - https://adapty.io/docs/get-pb-paywalls.md - https://adapty.io/docs/ios-present-paywalls.md - https://adapty.io/docs/ios-handling-events.md - https://adapty.io/docs/handle-paywall-actions.md ``` :::tip[Checkpoint] - **Expected:** Paywall appears with your configured products. Tapping a product triggers the sandbox purchase dialog. - **Gotcha:** Empty paywall or `getPaywall` error → verify placement ID matches the dashboard exactly and the placement has an audience assigned. ::: **Guides:** - [Enable purchases in your custom paywall (quickstart)](ios-quickstart-manual.md) - [Fetch paywalls and products](fetch-paywalls-and-products.md) - [Render paywall designed by remote config](present-remote-config-paywalls.md) - [Make purchases](making-purchases.md) - [Restore purchases](restore-purchase.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/ios-quickstart-manual.md - https://adapty.io/docs/fetch-paywalls-and-products.md - https://adapty.io/docs/present-remote-config-paywalls.md - https://adapty.io/docs/making-purchases.md - https://adapty.io/docs/restore-purchase.md ``` :::tip[Checkpoint] - **Expected:** Your custom paywall displays products fetched from Adapty. Tapping a product triggers the sandbox purchase dialog. - **Gotcha:** Empty products array → verify the paywall has products assigned in the dashboard and the placement has an audience. ::: **Guides:** - [Observer mode overview](observer-vs-full-mode.md) - [Implement Observer mode](implement-observer-mode.md) - [Report transactions in Observer mode](report-transactions-observer-mode.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/observer-vs-full-mode.md - https://adapty.io/docs/implement-observer-mode.md - https://adapty.io/docs/report-transactions-observer-mode.md ``` :::tip[Checkpoint] - **Expected:** After a sandbox purchase using your existing purchase flow, the transaction appears in the Adapty dashboard **Event Feed**. - **Gotcha:** No events → verify you're reporting transactions to Adapty and App Store Server Notifications are configured. ::: ### Check subscription status After a purchase, check the user profile for an active access level to gate premium content. **Guide:** [Check subscription status](ios-check-subscription-status.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/ios-check-subscription-status.md ``` :::tip[Checkpoint] - **Expected:** After a sandbox purchase, `profile.accessLevels["premium"]?.isActive` returns `true`. - **Gotcha:** Empty `accessLevels` after purchase → check the product has an access level assigned in the dashboard. ::: ### Identify users Link your app user accounts to Adapty profiles so purchases persist across devices. :::important Skip this step if your app has no authentication. ::: **Guide:** [Identify users](ios-quickstart-identify.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/ios-quickstart-identify.md ``` :::tip[Checkpoint] - **Expected:** After calling `Adapty.identify("your-user-id")`, the dashboard **Profiles** section shows your custom user ID. - **Gotcha:** Call `identify` after activation but before fetching paywalls to avoid anonymous profile attribution. ::: ### Prepare for release Once your integration works in the sandbox, walk through the release checklist to make sure everything is production-ready. **Guide:** [Release checklist](release-checklist.md) Send this to your LLM: ``` Read these Adapty docs before releasing: - https://adapty.io/docs/release-checklist.md ``` :::tip[Checkpoint] - **Expected:** All checklist items confirmed: store connection, server notifications, purchase flow, access level checks, and privacy requirements. - **Gotcha:** Missing App Store Server Notifications → configure them in **App settings → iOS SDK** or events won't appear in the dashboard. ::: ## Plain text doc index files If you need to give your LLM broader context beyond individual pages, we host index files that list or combine all Adapty documentation: - [`llms.txt`](https://adapty.io/docs/llms.txt): Lists all pages with `.md` links. An [emerging standard](https://llmstxt.org/) for making websites accessible to LLMs. Note that for some AI agents (e.g., ChatGPT) you will need to download `llms.txt` and upload it to the chat as a file. - [`llms-full.txt`](https://adapty.io/docs/llms-full.txt): The entire Adapty documentation site combined into a single file. Very large — use only when you need the full picture. - iOS-specific [`ios-llms.txt`](https://adapty.io/docs/ios-llms.txt) and [`ios-llms-full.txt`](https://adapty.io/docs/ios-llms-full.txt): Platform-specific subsets that save tokens compared to the full site. --- # File: get-pb-paywalls --- --- title: "Fetch Paywall Builder paywalls and their configuration in iOS SDK" description: "Learn how to retrieve PB paywalls in Adapty for better subscription control in your iOS app." --- After [you designed the visual part for your paywall](adapty-paywall-builder) with the new Paywall Builder in the Adapty Dashboard, you can display it in your mobile app. The first step in this process is to get the paywall associated with the placement and its view configuration as described below. :::warning The new Paywall Builder works with iOS SDK version 3.0 or higher. For presenting paywalls in Adapty SDK v2 designed with the legacy Paywall Builder, see [Display paywalls designed with legacy Paywall Builder](adapty-paywall-builder.md). ::: Please be aware that this topic refers to Paywall Builder-customized paywalls. If you are implementing your paywalls manually, please refer to the [Fetch paywalls and products for remote config paywalls in your mobile app](fetch-paywalls-and-products) topic. :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. :::
Before you start displaying paywalls in your mobile app (click to expand) 1. [Create your products](create-product) in the Adapty Dashboard. 2. [Create a paywall and incorporate the products into it](create-paywall) in the Adapty Dashboard. 3. [Create placements and incorporate your paywall into it](create-placement) in the Adapty Dashboard. 4. Install [Adapty SDK](sdk-installation-ios) in your mobile app.
## Fetch paywall designed with Paywall Builder If you've [designed a paywall using the Paywall Builder](adapty-paywall-builder), you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. Nevertheless, you need to get its ID via the placement, its view configuration, and then present it in your mobile app. To ensure optimal performance, it's crucial to retrieve the paywall and its [view configuration](get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) as early as possible, allowing sufficient time for images to download before presenting them to the user. To get a paywall, use the `getPaywall` method: ```swift showLineNumbers do { let paywall = try await Adapty.getPaywall("YOUR_PLACEMENT_ID") // the requested paywall } catch { // handle the error } ``` ```swift showLineNumbers Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en") { result in switch result { case let .success(paywall): // the requested paywall case let .failure(error): // handle the error } } ``` Parameters: | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the [paywall localization](add-paywall-locale-in-adapty-paywall-builder). This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.

| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

Adapty SDK stores paywalls locally in two layers: regularly updated cache described above and [fallback paywalls](fallback-paywalls). We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.

| | **loadTimeout** | default: 5 sec |

This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.

Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.

| Response parameters: | Parameter | Description | | :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](https://swift.adapty.io/documentation/adapty/adaptypaywall) object with a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch the view configuration of paywall designed using Paywall Builder :::important Make sure to enable the **Show on device** toggle in the paywall builder. If this option isn't turned on, the view configuration won't be available to retrieve. ::: After fetching the paywall, check if it includes a view configuration, which indicates that it was created using Paywall Builder. This will guide you on how to display the paywall. If the view configuration is present, treat it as a Paywall Builder paywall; if not, [handle it as a remote config paywall](present-remote-config-paywalls). Use the `getPaywallConfiguration` method to load the view configuration. ```swift showLineNumbers guard paywall.hasViewConfiguration else { // use your custom logic return } do { let paywallConfiguration = try await AdaptyUI.getPaywallConfiguration( forPaywall: paywall, products: products ) // use loaded configuration } catch { // handle the error } ``` Parameters: | Parameter | Presence | Description | | :----------------------- | :------------- | :----------------------------------------------------------- | | **paywall** | required | An `AdaptyPaywall` object to obtain a controller for the desired paywall. | | **loadTimeout** | default: 5 sec | This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood. | | **products** | optional | Provide an array of `AdaptyPaywallProducts` to optimize the display timing of products on the screen. If `nil` is passed, AdaptyUI will automatically fetch the necessary products. | :::note If you are using multiple languages, learn how to add a [Paywall Builder localization](add-paywall-locale-in-adapty-paywall-builder) and how to use locale codes correctly [here](localizations-and-locale-codes). ::: Once loaded, [present the paywall](ios-present-paywalls). ## Get a paywall for a default audience to fetch it faster Typically, paywalls are fetched almost instantly, so you don't need to worry about speeding up this process. However, in cases where you have numerous audiences and paywalls, and your users have a weak internet connection, fetching a paywall may take longer than you'd like. In such situations, you might want to display a default paywall to ensure a smooth user experience rather than showing no paywall at all. To address this, you can use the `getPaywallForDefaultAudience` method, which fetches the paywall of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the paywall by the `getPaywall` method, as detailed in the [Fetch Paywall Information](get-pb-paywalls#fetch-paywall-designed-with-paywall-builder) section above. :::warning Why we recommend using `getPaywall` The `getPaywallForDefaultAudience` method comes with a few significant drawbacks: - **Potential backward compatibility issues**: If you need to show different paywalls for different app versions (current and future), you may face challenges. You'll either have to design paywalls that support the current (legacy) version or accept that users with the current (legacy) version might encounter issues with non-rendered paywalls. - **Loss of targeting**: All users will see the same paywall designed for the **All Users** audience, which means you lose personalized targeting (including based on countries, marketing attribution or your own custom attributes). If you're willing to accept these drawbacks to benefit from faster paywall fetching, use the `getPaywallForDefaultAudience` method as follows. Otherwise stick to `getPaywall` described [above](get-pb-paywalls#fetch-paywall-designed-with-paywall-builder). ::: ```swift showLineNumbers Adapty.getPaywallForDefaultAudience(placementId: "YOUR_PLACEMENT_ID", locale: "en") { result in switch result { case let .success(paywall): // the requested paywall case let .failure(error): // handle the error } } ``` :::note The `getPaywallForDefaultAudience` method is available starting from iOS SDK version 2.11.2. ::: | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the [Placement](placements). This is the value you specified when creating a placement in your Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.

| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

| ## Customize assets To customize images and videos in your paywall, implement the custom assets. Hero images and videos have predefined IDs: `hero_image` and `hero_video`. In a custom asset bundle, you target these elements by their IDs and customize their behavior. For other images and videos, you need to [set a custom ID](https://adapty.io/docs/custom-media) in the Adapty dashboard. For example, you can: - Show a different image or video to some users. - Show a local preview image while a remote main image is loading. - Show a preview image before running a video. :::important To use this feature, update the Adapty iOS SDK to version 3.7.0 or higher. ::: Here's an example of how you can provide custom assets via a simple dictionary: ```swift showLineNumbers let customAssets: [String: AdaptyCustomAsset] = [ // Show a local image using a custom ID "custom_image": .image( .uiImage(value: UIImage(named: "image_name")!) ), // Show a local preview image while a remote main image is loading "hero_image": .image( .remote( url: URL(string: "https://example.com/image.jpg")!, preview: UIImage(named: "preview_image") ) ), // Show a local video with a preview image "hero_video": .video( .file( url: Bundle.main.url(forResource: "custom_video", withExtension: "mp4")!, preview: .uiImage(value: UIImage(named: "video_preview")!) ) ), ] let paywallConfig = try await AdaptyUI.getPaywallConfiguration( forPaywall: paywall, assetsResolver: customAssets ) ``` :::note If an asset is not found, the paywall will fall back to its default appearance. ::: ## Set up developer-defined timers To use custom timers in your mobile app, create an object that follows the `AdaptyTimerResolver` protocol. This object defines how each custom timer should be rendered. If you prefer, you can use a `[String: Date]` dictionary directly, as it already conforms to this protocol. Here is an example: ```swift showLineNumbers @MainActor struct AdaptyTimerResolverImpl: AdaptyTimerResolver { func timerEndAtDate(for timerId: String) -> Date { switch timerId { case "CUSTOM_TIMER_6H": Date(timeIntervalSinceNow: 3600.0 * 6.0) // 6 hours case "CUSTOM_TIMER_NY": Calendar.current.date(from: DateComponents(year: 2025, month: 1, day: 1)) ?? Date(timeIntervalSinceNow: 3600.0) default: Date(timeIntervalSinceNow: 3600.0) // 1 hour } } } ``` In this example, `CUSTOM_TIMER_NY` and `CUSTOM_TIMER_6H` are the **Timer ID**s of developer-defined timers you set in the Adapty Dashboard. The `timerResolver` ensures your app dynamically updates each timer with the correct value. For example: - `CUSTOM_TIMER_NY`: The time remaining until the timer's end, such as New Year's Day. - `CUSTOM_TIMER_6H`: The time left in a 6-hour period that started when the user opened the paywall. --- # File: ios-present-paywalls --- --- title: "Present new Paywall Builder paywalls in iOS SDK" description: "Discover how to present paywalls on iOS to boost conversions and revenue." --- If you've customized a paywall using the Paywall Builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. :::warning This guide is for **[new Paywall Builder paywalls](adapty-paywall-builder.md)** . The process for presenting paywalls differs for paywalls designed with different versions of Paywall Builder, remote config paywalls, and [Observer mode](observer-vs-full-mode). - For presenting **Legacy Paywall Builder paywalls**, check out [iOS - Present legacy Paywall Builder paywalls](ios-present-paywalls-legacy). - For presenting **Remote config paywalls**, see [Render paywall designed by remote config](present-remote-config-paywalls). - For presenting **Observer mode paywalls**, see [iOS - Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode) ::: To get the `AdaptyUI.PaywallConfiguration` object used below, see [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls). ## Present paywalls in SwiftUI ### Present as a modal view In order to display the visual paywall on the device screen as a modal view, use the `.paywall` modifier in SwiftUI: ```swift showLineNumbers title="SwiftUI" @State var paywallPresented = false // ensure that you manage this variable state and set it to `true` at the moment you want to show the paywall var body: some View { Text("Hello, AdaptyUI!") .paywall( isPresented: $paywallPresented, paywallConfiguration: , didPerformAction: { action in switch action { case .close: paywallPresented = false default: // Handle other actions break } }, didFinishPurchase: { product, profile in paywallPresented = false }, didFailPurchase: { product, error in /* handle the error */ }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, didFailRendering: { error in paywallPresented = false } ) } ``` Parameters: | Parameter | Required | Description | |:----------------------------------|:---------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **isPresented** | required | A binding that manages whether the paywall screen is displayed. | | **paywallConfiguration** | required | An `AdaptyUI.PaywallConfiguration` object containing visual details of the paywall. Use the `AdaptyUI.paywallConfiguration(for:products:viewConfiguration:observerModeResolver:tagResolver:timerResolver:)` method. Refer to [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls) topic for more details. | | **didFailPurchase** | required | Invoked when `Adapty.makePurchase()` fails. | | **didFinishRestore** | required | Invoked when `Adapty.restorePurchases()` completes successfully. | | **didFailRestore** | required | Invoked when `Adapty.restorePurchases()` fails. | | **didFailRendering** | required | Invoked if an error occurs while rendering the interface. In this case, [contact Adapty Support](mailto:support@adapty.io). | | **fullScreen** | optional | Determines if the paywall appears in full-screen mode or as a modal. Defaults to `true`. | | **didAppear** | optional | Invoked when the paywall view was presented. | | **didDisappear** | optional | Invoked when the paywall view was dismissed. | | **didPerformAction** | optional | Invoked when a user clicks a button. Different buttons have different action IDs. Two action IDs are pre-defined: `close` and `openURL`, while others are custom and can be set in the builder. | | **didSelectProduct** | optional | If the product was selected for purchase (by a user or by the system), this callback will be invoked. | | **didStartPurchase** | optional | Invoked when the user begins the purchase process. | | **didFinishPurchase** | optional | Invoked when `Adapty.makePurchase()` completes successfully. | | **didFinishWebPaymentNavigation** | optional | Invoked when web payment navigation finishes. | | **didStartRestore** | optional | Invoked when the user starts the restore process. | | **didFailLoadingProducts** | optional | Invoked when errors occur during product loading. Return `true` to retry loading. | | **didPartiallyLoadProducts** | optional | Invoked when products are partially loaded. | | **showAlertItem** | optional | A binding that manages the display of alert items above the paywall. | | **showAlertBuilder** | optional | A function for rendering the alert view. | | **placeholderBuilder** | optional | A function for rendering the placeholder view while the paywall is loading. | Refer to the [iOS - Handling events](ios-handling-events) topic for more details on parameters. ### Present as a non-modal view You can also present paywalls as navigation destinations or inline views within your app's navigation flow. Use `AdaptyPaywallView` directly in your SwiftUI views: ```swift showLineNumbers title="SwiftUI" AdaptyPaywallView( paywallConfiguration: , didFailPurchase: { product, error in // Handle purchase failure }, didFinishRestore: { profile in // Handle successful restore }, didFailRestore: { error in // Handle restore failure }, didFailRendering: { error in // Handle rendering error } ) ``` ## Present paywalls in UIKit In order to display the visual paywall on the device screen, do the following: 1. Initialize the visual paywall you want to display by using the `.paywallController(for:products:viewConfiguration:delegate:)` method: ```swift showLineNumbers title="Swift" import Adapty import AdaptyUI let visualPaywall = AdaptyUI.paywallController( with: , delegate: ) ``` Request parameters: | Parameter | Presence | Description | | :----------------------- | :------- | :---------- | | **paywall configuration** | required | An `AdaptyUI.PaywallConfiguration` object containing visual details of the paywall. Use the `AdaptyUI.getPaywallConfiguration(forPaywall:locale:)` method. Refer to [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls) topic for more details. | | **delegate** | required | An `AdaptyPaywallControllerDelegate` to listen to paywall events. Refer to [Handling paywall events](ios-handling-events) topic for more details. Returns: | Object | Description | | :---------------------- | :--------------------------------------------------- | | **AdaptyPaywallController** | An object, representing the requested paywall screen | 2. After the object has been successfully created, you can display it on the screen of the device: ```swift showLineNumbers title="Swift" present(visualPaywall, animated: true) ``` :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: --- # File: handle-paywall-actions --- --- title: "Respond to button actions in iOS SDK" description: "Handle paywall button actions in iOS using Adapty for better app monetization." --- If you are building paywalls using the Adapty paywall builder, it's crucial to set up buttons properly: 1. Add a [button in the paywall builder](paywall-buttons.md) and assign it either a pre-existing action or create a custom action ID. 2. Write code in your app to handle each action you've assigned. This guide shows how to handle custom and pre-existing actions in your code. :::warning **Only purchases, restorations, paywall closures, and URL opening are handled automatically.** All other button actions require proper response implementation in the app code. ::: ## Close paywalls To add a button that will close your paywall: 1. In the paywall builder, add a button and assign it the **Close** action. 2. In your app code, implement a handler for the `close` action that dismisses the paywall. :::info In the iOS SDK, the `close` action triggers closing the paywall by default. However, you can override this behavior in your code if needed. For example, closing one paywall might trigger opening another. ::: ```swift func paywallController(_ controller: AdaptyPaywallController, didPerform action: AdaptyUI.Action) { switch action { case .close: controller.dismiss(animated: true) // default behavior break } } ``` ## Open URLs from paywalls :::tip If you want to add a group of links (e.g., terms of use and purchase restoration), add a **Link** element in the paywall builder and handle it the same way as buttons with the **Open URL** action. ::: To add a button that opens a link from your paywall (e.g., **Terms of use** or **Privacy policy**): 1. In the paywall builder, add a button, assign it the **Open URL** action, and enter the URL you want to open. 2. In your app code, implement a handler for the `openUrl` action that opens the received URL in a browser. :::info In the iOS SDK, the `openUrl` action triggers opening the URL by default. However, you can override this behavior in your code if needed. ::: ```swift func paywallController(_ controller: AdaptyPaywallController, didPerform action: AdaptyUI.Action) { switch action { case let .openURL(url): UIApplication.shared.open(url, options: [:]) // default behavior break } } ``` ## Log into the app To add a button that logs users into your app: 1. In the paywall builder, add a button and assign it the **Login** action. 2. In your app code, implement a handler for the `login` action that identifies your user. ```swift func paywallController(_ controller: AdaptyPaywallController, didPerform action: AdaptyUI.Action) { switch action { case .login: // Show a login screen let loginVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "LoginViewController") controller.present(loginVC, animated: true) } } ``` ## Handle custom actions To add a button that handles any other actions: 1. In the paywall builder, add a button, assign it the **Custom** action, and assign it an ID. 2. In your app code, implement a handler for the action ID you've created. For example, if you have another set of subscription offers or one-time purchases, you can add a button that will display another paywall: ```swift func paywallController(_ controller: AdaptyPaywallController, didPerform action: AdaptyUI.Action) { switch action { case let .custom(id): if id == "openNewPaywall" { // Display another paywall } } break } } ``` --- # File: ios-handling-events --- --- title: "Handle paywall events in iOS SDK" description: "Handle subscription-related events in iOS using Adapty for better app monetization." --- :::important This guide covers event handling for purchases, restorations, product selection, and paywall rendering. You must also implement button handling (closing paywall, opening links, etc.). See our [guide on handling button actions](https://adapty.io/docs/handle-paywall-actions) for details. ::: Paywalls configured with the [Paywall Builder](adapty-paywall-builder) don't need extra code to make and restore purchases. However, they generate some events that your app can respond to. Those events include button presses (close buttons, URLs, product selections, and so on) as well as notifications on purchase-related actions taken on the paywall. Learn how to respond to these events below. This guide is for **new Paywall Builder paywalls** only which require Adapty SDK v3.0 or later. For presenting paywalls in Adapty SDK v2 designed with legacy Paywall Builder, see [iOS - Handle paywall events designed with legacy Paywall Builder](ios-handling-events-legacy). :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ## Handling events in SwiftUI To control or monitor processes occurring on the paywall screen within your mobile app, use the `.paywall` modifier in SwiftUI: ```swift showLineNumbers title="Swift" @State var paywallPresented = false var body: some View { Text("Hello, AdaptyUI!") .paywall( isPresented: $paywallPresented, paywall: paywall, viewConfiguration: viewConfig, didPerformAction: { action in switch action { case .close: paywallPresented = false case let .openURL(url): // handle opening the URL (incl. for terms and privacy) default: // handle other actions } }, didSelectProduct: { /* Handle the event */ }, didStartPurchase: { /* Handle the event */ }, didFinishPurchase: { product, info in /* Handle the event */ }, didFailPurchase: { product, error in /* Handle the event */ }, didStartRestore: { /* Handle the event */ }, didFinishRestore: { /* Handle the event */ }, didFailRestore: { /* Handle the event */ }, didFailRendering: { error in paywallPresented = false }, didFailLoadingProducts: { error in return false } ) } ``` You can register only the closure parameters you need, and omit those you do not need. In this case, unused closure parameters will not be created. | Parameter | Required | Description | |:----------------------------------|:---------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **isPresented** | required | A binding that manages whether the paywall screen is displayed. | | **paywallConfiguration** | required | An `AdaptyUI.PaywallConfiguration` object containing visual details of the paywall. Use the `AdaptyUI.paywallConfiguration(for:products:viewConfiguration:observerModeResolver:tagResolver:timerResolver:)` method. Refer to [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls) topic for more details. | | **didFailPurchase** | required | Invoked when a purchase fails due to errors (e.g., payment not allowed, network issues, invalid product). Not invoked for user cancellations or pending payments. | | **didFinishRestore** | required | Invoked when purchase completes successfully. | | **didFailRestore** | required | Invoked when restoring a purchase fails. | | **didFailRendering** | required | Invoked if an error occurs while rendering the interface. In this case, [contact Adapty Support](mailto:support@adapty.io). | | **fullScreen** | optional | Determines if the paywall appears in full-screen mode or as a modal. Defaults to `true`. | | **didAppear** | optional | Invoked when the paywall view appears on screen. Also invoked when a user taps the [web paywall button](web-paywall#step-2a-add-a-web-purchase-button) inside a paywall, and a web paywall opens in an in-app browser. | | **didDisappear** | optional | Invoked when the paywall view was dismissed. Also invoked when a [web paywall](web-paywall#step-2a-add-a-web-purchase-button) opened from a paywall in an in-app browser disappears from the screen. | | **didPerformAction** | optional | Invoked when a user clicks a button. Different buttons have different action IDs. Two action IDs are pre-defined: `close` and `openURL`, while others are custom and can be set in the builder. | | **didSelectProduct** | optional | If the product was selected for purchase (by a user or by the system), this callback will be invoked. | | **didStartPurchase** | optional | Invoked when the user begins the purchase process. | | **didFinishPurchase** | optional | Invoked when purchase completes successfully. | | **didFinishWebPaymentNavigation** | optional | Invoked after attempting to open a [web paywall](web-paywall) for purchase, whether successful or failed. | | **didStartRestore** | optional | Invoked when the user starts the restore process. | | **didFailLoadingProducts** | optional | Invoked when errors occur during product loading. Return `true` to retry loading. | | **didPartiallyLoadProducts** | optional | Invoked when products are partially loaded. | | **showAlertItem** | optional | A binding that manages the display of alert items above the paywall. | | **showAlertBuilder** | optional | A function for rendering the alert view. | | **placeholderBuilder** | optional | A function for rendering the placeholder view while the paywall is loading. | ## Handling events in UIKit To control or monitor processes occurring on the paywall screen within your mobile app, implement the `AdaptyPaywallControllerDelegate` methods. ### User-generated events #### Product selection If a user selects a product for purchase, this method will be invoked: ```swift showLineNumbers title="Swift" func paywallController( _ controller: AdaptyPaywallController, didSelectProduct product: AdaptyPaywallProductWithoutDeterminingOffer ) { } ```
Event example (Click to expand) ```javascript { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } ```
#### Started purchase If a user initiates the purchase process, this method will be invoked: ```swift showLineNumbers title="Swift" func paywallController(_ controller: AdaptyPaywallController, didStartPurchase product: AdaptyPaywallProduct) { } ```
Event example (Click to expand) ```javascript { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } ```
It will not be invoked in Observer mode. Refer to the [iOS - Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode) topic for details. #### Started purchase using a web paywall If a user initiates the purchase process using a [web paywall](web-paywall.md), this method will be invoked: ```swift showLineNumbers title="Swift" func paywallController( _ controller: AdaptyPaywallController, shouldContinueWebPaymentNavigation product: AdaptyPaywallProduct ) { } ```
Event example (Click to expand) ```javascript { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } ```
#### Successful or canceled purchase If purchase succeeds, this method will be invoked: ```swift showLineNumbers title="Swift" func paywallController( _ controller: AdaptyPaywallController, didFinishPurchase product: AdaptyPaywallProductWithoutDeterminingOffer, purchaseResult: AdaptyPurchaseResult ) { } } ```
Event examples (Click to expand) ```javascript // Successful purchase { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "purchaseResult": { "type": "success", "profile": { "accessLevels": { "premium": { "id": "premium", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } } } } } // Cancelled purchase { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "purchaseResult": { "type": "cancelled" } } ```
We recommend dismissing the paywall screen in that case. It will not be invoked in Observer mode. Refer to the [iOS - Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode) topic for details. #### Failed purchase If a purchase fails due to an error, this method will be invoked. This includes StoreKit errors (payment restrictions, invalid products, network failures), transaction verification failures, and system errors. Note that user cancellations trigger `didFinishPurchase` with a cancelled result instead, and pending payments do not trigger this method. ```swift showLineNumbers title="Swift" func paywallController( _ controller: AdaptyPaywallController, didFailPurchase product: AdaptyPaywallProduct, error: AdaptyError ) { } ```
Event example (Click to expand) ```javascript { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "error": { "code": "purchase_failed", "message": "Purchase failed due to insufficient funds", "details": { "underlyingError": "Insufficient funds in account" } } } ```
It will not be invoked in Observer mode. Refer to the [iOS - Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode) topic for details. #### Failed purchase using a web paywall If `Adapty.openWebPaywall()` fails, this method will be invoked: ```swift showLineNumbers title="Swift" func paywallController( _ controller: AdaptyPaywallController, didFailWebPaymentNavigation product: AdaptyPaywallProduct, error: AdaptyError ) { } ```
Event example (Click to expand) ```javascript { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "error": { "code": "web_payment_failed", "message": "Web payment navigation failed", "details": { "underlyingError": "Network connection error" } } } ```
#### Successful restore If restoring a purchase succeeds, this method will be invoked: ```swift showLineNumbers title="Swift" func paywallController( _ controller: AdaptyPaywallController, didFinishRestoreWith profile: AdaptyProfile ) { } ```
Event example (Click to expand) ```javascript { "profile": { "accessLevels": { "premium": { "id": "premium", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } }, "subscriptions": [ { "vendorProductId": "premium_monthly", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } ] } } ```
We recommend dismissing the screen if a the has the required `accessLevel`. Refer to the [Subscription status](subscription-status) topic to learn how to check it. #### Failed restore If restoring a purchase fails, this method will be invoked: ```swift showLineNumbers title="Swift" public func paywallController( _ controller: AdaptyPaywallController, didFailRestoreWith error: AdaptyError ) { } ```
Event example (Click to expand) ```javascript { "error": { "code": "restore_failed", "message": "Purchase restoration failed", "details": { "underlyingError": "No previous purchases found" } } } ```
### Data fetching and rendering #### Product loading errors If you don't pass the product array during the initialization, AdaptyUI will retrieve the necessary objects from the server by itself. If this operation fails, AdaptyUI will report the error by calling this method: ```swift showLineNumbers title="Swift" public func paywallController( _ controller: AdaptyPaywallController, didFailLoadingProductsWith error: AdaptyError ) -> Bool { return true } ```
Event example (Click to expand) ```javascript { "error": { "code": "products_loading_failed", "message": "Failed to load products from the server", "details": { "underlyingError": "Network timeout" } } } ```
If you return `true`, AdaptyUI will repeat the request after 2 seconds. #### Rendering errors If an error occurs during the interface rendering, it will be reported by this method: ```swift showLineNumbers title="Swift" public func paywallController( _ controller: AdaptyPaywallController, didFailRenderingWith error: AdaptyError ) { } ```
Event example (Click to expand) ```javascript { "error": { "code": "rendering_failed", "message": "Failed to render paywall interface", "details": { "underlyingError": "Invalid paywall configuration" } } } ```
In a normal situation, such errors should not occur, so if you come across one, please let us know. --- # File: ios-use-fallback-paywalls --- --- title: "iOS - Use fallback paywalls" description: "Handle cases when users are offline or Adapty servers aren't available" --- :::warning Fallback paywalls are supported by iOS SDK v2.11 or later. ::: To maintain a fluid user experience, it is important to set up [fallbacks](/fallback-paywalls) for your [paywalls](paywalls) and [onboardings](onboardings). This precaution extends the application's capabilities in case of partial or complete loss of internet connection. * **If the application cannot access Adapty servers:** It will be able to display a fallback paywall, and access the local onboarding configuration. * **If the application cannot access the internet:** It will be able to display a fallback paywall. Onboardings include remote content and require an internet connection to function. :::important Before you follow the steps in this guide, [download](/local-fallback-paywalls) the fallback configuration files from Adapty. ::: ## Configuration 1. Add the fallback JSON file to your project bundle: open the **File** menu in XCode and select the **Add Files to "YourProjectName"** option. 2. Call the `.setFallback` method **before** you fetch the target paywall or onboarding. ```swift showLineNumbers do { if let urlPath = Bundle.main.url(forResource: fileName, withExtension: "json") { try await Adapty.setFallback(fileURL: urlPath) } } catch { // handle the error } ``` ```swift showLineNumbers if let url = Bundle.main.url(forResource: "ios_fallback", withExtension: "json") { Adapty.setFallback(fileURL: url) } ``` Parameters: | Parameter | Description | | :---------- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **fileURL** | Path to the fallback configuration file. | --- # File: localizations-and-locale-codes --- --- title: "Use localizations and locale codes in iOS SDK" description: "Manage app localizations and locale codes to reach a global audience in your iOS app." --- ## Why this is important There are a few scenarios when locale codes come into play — for example, when you're trying to fetch the correct paywall for the current localization of your app. As locale codes are complicated and can vary from platform to platform, we rely on an internal standard for all the platforms we support. However, because these codes are complicated, it is really important for you to understand what exactly are you sending to our server to get the correct localization, and what happens next — so you will always receive what you expect. ## Locale code standard at Adapty For locale codes, Adapty uses a slightly modified [BCP 47 standard](https://en.wikipedia.org/wiki/IETF_language_tag): every code consists of lowercase subtags, separated by hyphens. Some examples: `en` (English), `pt-br` (Portuguese (Brazil)), `zh` (Simplified Chinese), `zh-hant` (Traditional Chinese). ## Locale code matching When Adapty receives a call from the client-side SDK with the locale code and starts looking for a corresponding localization of a paywall, the following happens: 1. The incoming locale string is converted to lowercase and all the underscores (`_`) are replaced with hyphens (`-`) 2. We then look for the localization with the fully matching locale code 3. If no match was found, we take the substring before the first hyphen (`pt` for `pt-br`) and look for the matching localization 4. If no match was found again, we return the default `en` localization This way an iOS device that sent `'pt_BR'`, an Android device that sent `pt-BR`, and another device that sent `pt-br` will get the same result. ## Implementing localizations: recommended way If you're wondering about localizations, chances are you're already dealing with the localized string files in your project. If that's the case, we recommend placing some key-value with the intended Adapty locale code in each of your files for the corresponding localizations. And then extract the value for this key when calling our SDK, like so: ```swift showLineNumbers // 1. Modify your Localizable.strings files /* Localizable.strings - Spanish */ adapty_paywalls_locale = "es"; /* Localizable.strings - Portuguese (Brazil) */ adapty_paywalls_locale = "pt-br"; // 2. Extract and use the locale code let locale = NSLocalizedString("adapty_paywalls_locale", comment: "") // pass locale code to AdaptyUI.getViewConfiguration or Adapty.getPaywall method ``` That way you can ensure you're in full control of what localization will be retrieved for every user of your app. ## Implementing localizations: the other way You can get similar (but not identical) results without explicitly defining locale codes for every localization. That would mean extracting a locale code from some other objects that your platform provides, like this: ```swift showLineNumbers let locale = Locale.current.identifier // pass locale code to AdaptyUI.getViewConfiguration or Adapty.getPaywall method ``` Note that we don't recommend this approach due to few reasons: 1. On iOS preferred languages and current locale are not identical. If you want the localization to be picked correctly you'll have to either rely on Apple's logic, which works out of the box if you're using the recommended approach with localized string files, or re-create it. 2. It's hard to predict what exactly will Adapty's server get. For example, on iOS, it is possible to obtain a locale like `ar_OM@numbers='latn'` on a device and send it to our server. And for this call you will get not the `ar-om` localization you were looking for, but rather `ar`, which is likely unexpected. Should you decide to use this approach anyway — make sure you've covered all the relevant use cases. --- # File: ios-web-paywall --- --- title: "Implement web paywalls in iOS SDK" description: "Set up a web paywall to get paid without the App Store fees and audits." --- :::important Before you begin, make sure you have [configured your web paywall in the dashboard](web-paywall.md) and installed Adapty SDK version 3.6.1 or later. ::: ## Open web paywalls If you are working with a paywall you developed yourself, you need to handle web paywalls using the SDK method. The `.openWebPaywall` method: 1. Generates a unique URL allowing Adapty to link a specific paywall shown to a particular user to the web page they are redirected to. 2. Tracks when your users return to the app and then requests `.getProfile` at short intervals to determine whether the profile access rights have been updated. This way, if the payment has been successful and access rights have been updated, the subscription activates in the app almost immediately. ```swift showLineNumbers title="Swift" do { try await Adapty.openWebPaywall(for: product) } catch { print("Failed to open web paywall: \(error)") } ``` :::note There are two versions of the `openWebPaywall` method: 1. `openWebPaywall(product)` that generates URLs by paywall and adds the product data to URLs as well. 2. `openWebPaywall(paywall)` that generates URLs by paywall without adding the product data to URLs. Use it when your products in the Adapty paywall differ from those in the web paywall. ::: ## Handle errors | Error | Description | Recommended action | |-----------------------------------------|--------------------------------------------------------|---------------------------------------------------------------------------| | AdaptyError.paywallWithoutPurchaseUrl | The paywall doesn't have a web purchase URL configured | Check if the paywall has been properly configured in the Adapty Dashboard | | AdaptyError.productWithoutPurchaseUrl | The product doesn't have a web purchase URL | Verify the product configuration in the Adapty Dashboard | | AdaptyError.failedOpeningWebPaywallUrl | Failed to open the URL in the browser | Check device settings or provide an alternative purchase method | | AdaptyError.failedDecodingWebPaywallUrl | Failed to properly encode parameters in the URL | Verify URL parameters are valid and properly formatted | ## Implementation example ```swift showLineNumbers title="Swift" class SubscriptionViewController: UIViewController { var paywall: AdaptyPaywall? @IBAction func purchaseButtonTapped(_ sender: UIButton) { guard let paywall = paywall, let product = paywall.products.first else { return } Task { await offerWebPurchase(for: product) } } func offerWebPurchase(for paywallProduct: AdaptyPaywallProduct) async { do { // Attempt to open web paywall try await Adapty.openWebPaywall(for: paywallProduct) } catch let error as AdaptyError { switch error { case .paywallWithoutPurchaseUrl, .productWithoutPurchaseUrl: showAlert(message: "Web purchase is not available for this product.") case .failedOpeningWebPaywallUrl: showAlert(message: "Could not open web browser. Please try again.") default: showAlert(message: "An error occurred: \(error.localizedDescription)") } } catch { showAlert(message: "An unexpected error occurred.") } } // Helper methods private func showAlert(message: String) { /* ... */ } } ``` :::note After users return to the app, refresh the UI to reflect the profile updates. `AdaptyDelegate` will receive and process profile update events. ::: ## Open web paywalls in an in-app browser :::important Opening web paywalls in an in-app browser is supported starting from Adapty SDK v. 3.15. ::: By default, web paywalls open in the external browser. To provide a seamless user experience, you can open web paywalls in an in-app browser. This displays the web purchase page within your application, allowing users to complete transactions without switching apps. To enable this, set the `in` parameter to `.inAppBrowser`: ```swift showLineNumbers title="Swift" do { try await Adapty.openWebPaywall(for: product, in: .inAppBrowser) // default – .externalBrowser } catch { print("Failed to open web paywall: \(error)") } ``` --- # File: ios-troubleshoot-paywall-builder --- --- title: "Troubleshoot Paywall Builder in iOS SDK" description: "Troubleshoot Paywall Builder in iOS SDK" --- This guide helps you resolve common issues when using paywalls designed in the Adapty Paywall Builder in the iOS SDK. ## Getting a paywall configuration fails **Issue**: The `getPaywallConfiguration` method fails to retrieve paywall configuration. **Reason**: The paywall is not enabled for device display in the Paywall Builder. **Solution**: Enable the **Show on device** toggle in the Paywall Builder. ## The paywall view number is too big **Issue**: The paywall view count is showing double the expected number. **Reason**: You may be calling `logShowPaywall` in your code, which duplicates the view count if you're using the Paywall builder. For paywalls designed with the Paywall Builder, analytics are tracked automatically, so you don't need to use this method. **Solution**: Ensure you are not calling `logShowPaywall` in your code if you're using the Paywall builder. ## Other issues **Issue**: You're experiencing other Paywall Builder-related problems not covered above. **Solution**: Migrate the SDK to the latest version using the [migration guides](ios-sdk-migration-guides) if needed. Many issues are resolved in newer SDK versions. --- # File: ios-quickstart-manual --- --- title: "Enable purchases in your custom paywall in iOS SDK" description: "Integrate Adapty SDK into your custom iOS paywalls to enable in-app purchases." --- This guide describes how to integrate Adapty into your custom paywalls. Keep full control over paywall implementation, while the Adapty SDK fetches products, handles new purchases, and restores previous ones. :::important **This guide is for developers who are implementing custom paywalls.** If you want the easiest way to enable purchases, use the [Adapty Paywall Builder](ios-quickstart-paywalls.md). With Paywall Builder, you create paywalls in a no-code visual editor, Adapty handles all purchase logic automatically, and you can test different designs without republishing your app. ::: ## Before you start ### Set up products To enable in-app purchases, you need to understand three key concepts: - [**Products**](product.md) – anything users can buy (subscriptions, consumables, lifetime access) - [**Paywalls**](paywalls.md) – configurations that define which products to offer. In Adapty, paywalls are the only way to retrieve products, but this design lets you modify products, prices, and offers without touching your app code. - [**Placements**](placements.md) – where and when you show paywalls in your app (like `main`, `onboarding`, `settings`). You set up paywalls for placements in the dashboard, then request them by placement ID in your code. This makes it easy to run A/B tests and show different paywalls to different users. Make sure you understand these concepts even if you work with your custom paywall. Basically, they are just your way to manage the products you sell in your app. To implement your custom paywall, you will need to create a **paywall** and add it to a **placement**. This setup allows you to retrieve your products. To understand what you need to do in the dashboard, follow the quickstart guide [here](quickstart.md). ### Manage users You can work either with or without backend authentication on your side. However, the Adapty SDK handles anonymous and identified users differently. Read the [identification quickstart guide](ios-quickstart-identify.md) to understand the specifics and ensure you are working with users properly. ## Step 1. Get products To retrieve products for your custom paywall, you need to: 1. Get the `paywall` object by passing [placement](placements.md) ID to the `getPaywall` method. 2. Get the products array for this paywall using the `getPaywallProducts` method. ```swift func loadPaywall() async { do { let paywall = try await Adapty.getPaywall("YOUR_PLACEMENT_ID") let products = try await Adapty.getPaywallProducts(paywall: paywall) // Use products to build your custom paywall UI } catch { // Handle the error } } ``` ```swift func loadPaywall() { Adapty.getPaywall("YOUR_PLACEMENT_ID") { result in switch result { case let .success(paywall): Adapty.getPaywallProducts(paywall: paywall) { result in switch result { case let .success(products): // Use products to build your custom paywall UI case let .failure(error): // Handle the error } } case let .failure(error): // Handle the error } } } ``` ## Step 2. Accept purchases When a user taps on a product in your custom paywall, call the `makePurchase` method with the selected product. This will handle the purchase flow and return the updated profile. ```swift func purchaseProduct(_ product: AdaptyPaywallProduct) async { do { let purchaseResult = try await Adapty.makePurchase(product: product) switch purchaseResult { case .userCancelled: // User canceled the purchase break case .pending: // Purchase is pending (e.g., awaiting parental approval) break case let .success(profile, transaction): // Purchase successful, profile updated break } } catch { // Handle the error } } ``` ```swift func purchaseProduct(_ product: AdaptyPaywallProduct) { Adapty.makePurchase(product: product) { result in switch result { case let .success(purchaseResult): switch purchaseResult { case .userCancelled: // User canceled the purchase break case .pending: // Purchase is pending (e.g., awaiting parental approval) break case let .success(profile, transaction): // Purchase successful, profile updated break } case let .failure(error): // Handle the error } } } ``` ## Step 3. Restore purchases Apple requires all apps with subscriptions to provide a way users can restore their purchases. While purchases are automatically restored when a user logs in with their Apple ID, you must still implement a restore button in your app. Call the `restorePurchases` method when the user taps the restore button. This will sync their purchase history with Adapty and return the updated profile. ```swift func restorePurchases() async { do { let profile = try await Adapty.restorePurchases() // Restore successful, profile updated } catch { // Handle the error } } ``` ```swift func restorePurchases() { Adapty.restorePurchases { result in switch result { case let .success(profile): // Restore successful, profile updated case let .failure(error): // Handle the error } } } ``` ## Next steps Your paywall is ready to be displayed in the app. [Test your purchases in sandbox mode](test-purchases-in-sandbox) to make sure you can complete a test purchase from the paywall. Next, [check whether users have completed their purchase](ios-check-subscription-status.md) to determine whether to display the paywall or grant access to paid features. --- # File: fetch-paywalls-and-products --- --- title: "Fetch paywalls and products for remote config paywalls in iOS SDK" description: "Fetch paywalls and products in Adapty iOS SDK to enhance user monetization." --- Before showcasing remote config and custom paywalls, you need to fetch the information about them. Please be aware that this topic refers to remote config and custom paywalls. For guidance on fetching paywalls for Paywall Builder-customized paywalls, please consult the [iOS](get-pb-paywalls.md), [Android](android-get-pb-paywalls.md), [React Native](react-native-get-pb-paywalls.md), [Flutter](flutter-get-pb-paywalls.md), and [Unity](unity-get-pb-paywalls.md). :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. :::
Before you start fetching paywalls and products in your mobile app (click to expand) 1. [Create your products](create-product) in the Adapty Dashboard. 2. [Create a paywall and incorporate the products into your paywall](create-paywall) in the Adapty Dashboard. 3. [Create placements and incorporate your paywall into the placement](create-placement) in the Adapty Dashboard. 4. [Install Adapty SDK](sdk-installation-ios) in your mobile app.
## Fetch paywall information In Adapty, a [product](product) serves as a combination of products from both the App Store and Google Play. These cross-platform products are integrated into paywalls, enabling you to showcase them within specific mobile app placements. To display the products, you need to obtain a [Paywall](paywalls) from one of your [placements](placements) with `getPaywall` method. :::important **Don't hardcode product IDs.** The only ID you should hardcode is the placement ID. Paywalls are configured remotely, so the number of products and available offers can change at any time. Your app must handle these changes dynamically—if a paywall returns two products today and three tomorrow, display all of them without code changes. ::: ```swift showLineNumbers do { let paywall = try await Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID") // the requested paywall } catch { // handle the error } ``` ```swift showLineNumbers Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en") { result in switch result { case let .success(paywall): // the requested paywall case let .failure(error): // handle the error } } ``` | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the [Placement](placements). This is the value you specified when creating a placement in your Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.

| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

Adapty SDK stores paywalls in two layers: regularly updated cache described above and [fallback paywalls](fallback-paywalls) . We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.

| | **loadTimeout** | default: 5 sec |

This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.

Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.

| Don't hardcode product IDs! Since paywalls are configured remotely, the available products, the number of products, and special offers (such as free trials) can change over time. Make sure your code handles these scenarios. For example, if you initially retrieve 2 products, your app should display those 2 products. However, if you later retrieve 3 products, your app should display all 3 without requiring any code changes. The only thing you have to hardcode is placement ID. Response parameters: | Parameter | Description | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](https://swift.adapty.io/documentation/adapty/adaptypaywall) object with: a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch products Once you have the paywall, you can query the product array that corresponds to it: ```swift showLineNumbers do { let products = try await Adapty.getPaywallProducts(paywall: paywall) // the requested products array } catch { // handle the error } ``` ```swift showLineNumbers Adapty.getPaywallProducts(paywall: paywall) { result in switch result { case let .success(products): // the requested products array case let .failure(error): // handle the error } } ``` Response parameters: | Parameter | Description | | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Products | List of [`AdaptyPaywallProduct`](https://swift.adapty.io/documentation/adapty/adaptypaywallproduct) objects with: product identifier, product name, price, currency, subscription length, and several other properties. | When implementing your own paywall design, you will likely need access to these properties from the [`AdaptyPaywallProduct`](https://swift.adapty.io/documentation/adapty/adaptypaywallproduct) object. Illustrated below are the most commonly used properties, but refer to the linked document for full details on all available properties. | Property | Description | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Title** | To display the title of the product, use `product.localizedTitle`. Note that the localization is based on the users' selected store country rather than the locale of the device itself. | | **Price** | To display a localized version of the price, use `product.localizedPrice`. This localization is based on the locale info of the device. You can also access the price as a number using `product.price`. The value will be provided in the local currency. To get the associated currency symbol, use `product.currencySymbol`. | | **Subscription Period** | To display the period (e.g. week, month, year, etc.), use `product.localizedSubscriptionPeriod`. This localization is based on the locale of the device. To fetch the subscription period programmatically, use `product.subscriptionPeriod`. From there you can access the `unit` enum to get the length (i.e. day, week, month, year, or unknown). The `numberOfUnits` value will get you the number of period units. For example, for a quarterly subscription, you'd see `.month` in the unit property, and `3` in the numberOfUnits property. | | **Introductory Offer** | To display a badge or other indicator that a subscription contains an introductory offer, check out the `product.subscriptionOffer` property. Within this object are the following helpful properties:
• `offerType`: an enum with values `introductory`, `promotional`, and `winBack`. Free trials and initial discounted subscriptions will be the `introductory` type.
• `price`: The discounted price as a number. For free trials, look for `0` here.
• `localizedPrice`: A formatted price of the discount for the user's locale.
• `localizedNumberOfPeriods`: a string localized using the device's locale describing the length of the offer. For example, a three day trial offer shows `3 days` in this field.
• `subscriptionPeriod`: Alternatively, you can get the individual details of the offer period with this property. It works in the same manner for offers as the previous section describes.
• `localizedSubscriptionPeriod`: A formatted subscription period of the discount for the user's locale. | ## Check intro offer eligibility on iOS By default, the `getPaywallProducts` method checks eligibility for introductory, promotional, and win-back offers. If you need to display products before the SDK determines offer eligibility, use the `getPaywallProductsWithoutDeterminingOffer` method instead. :::note After showing the initial products, be sure to call the regular `getPaywallProducts` method to update the products with accurate offer eligibility information. ::: ```swift showLineNumbers do { let products = try await Adapty.getPaywallProductsWithoutDeterminingOffer(paywall: paywall) // the requested products array without subscriptionOffer } catch { // handle the error } ``` ```swift showLineNumbers Adapty.getPaywallProductsWithoutDeterminingOffer(paywall: paywall) { result in switch result { case let .success(products): // the requested products array without subscriptionOffer case let .failure(error): // handle the error } } ``` ## Speed up paywall fetching with default audience paywall Typically, paywalls are fetched almost instantly, so you don’t need to worry about speeding up this process. However, in cases where you have numerous audiences and paywalls, and your users have a weak internet connection, fetching a paywall may take longer than you'd like. In such situations, you might want to display a default paywall to ensure a smooth user experience rather than showing no paywall at all. To address this, you can use the `getPaywallForDefaultAudience` method, which fetches the paywall of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the paywall by the `getPaywall` method, as detailed in the [Fetch Paywall Information](fetch-paywalls-and-products#fetch-paywall-information) section above. :::warning Why we recommend using `getPaywall` The `getPaywallForDefaultAudience` method comes with a few significant drawbacks: - **Potential backward compatibility issues**: If you need to show different paywalls for different app versions (current and future), you may face challenges. You’ll either have to design paywalls that support the current (legacy) version or accept that users with the current (legacy) version might encounter issues with non-rendered paywalls. - **Loss of targeting**: All users will see the same paywall designed for the **All Users** audience, which means you lose personalized targeting (including based on countries, marketing attribution or your own custom attributes). If you're willing to accept these drawbacks to benefit from faster paywall fetching, use the `getPaywallForDefaultAudience` method as follows. Otherwise, stick to the `getPaywall` described [above](fetch-paywalls-and-products#fetch-paywall-information). ::: ```swift showLineNumbers do { let paywall = try await Adapty.getPaywallForDefaultAudience("YOUR_PLACEMENT_ID") // the requested paywall } catch { // handle the error } ``` ```swift showLineNumbers Adapty.getPaywallForDefaultAudience(placementId: "YOUR_PLACEMENT_ID", locale: "en") { result in switch result { case let .success(paywall): // the requested paywall case let .failure(error): // handle the error } } ``` :::note The `getPaywallForDefaultAudience` method is available starting from iOS SDK version 2.11.2. ::: | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the [Placement](placements). This is the value you specified when creating a placement in your Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.

| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

| --- # File: present-remote-config-paywalls --- --- title: "Render paywall designed by remote config in iOS SDK" description: "Discover how to present remote config paywalls in Adapty to personalize user experience." --- If you've customized a paywall using remote config, you'll need to implement rendering in your mobile app's code to display it to users. Since remote config offers flexibility tailored to your needs, you're in control of what's included and how your paywall view appears. We provide a method for fetching the remote configuration, giving you the autonomy to showcase your custom paywall configured via remote config. Don't forget to [check if a user is eligible for an introductory offer in iOS](fetch-paywalls-and-products#check-intro-offer-eligibility-on-ios) and adjust the paywall view to process the case when they are eligible. ## Get paywall remote config and present it To get a remote config of a paywall, access the `remoteConfig` property and extract the needed values. ```swift showLineNumbers do { let paywall = try await Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID") let headerText = paywall.remoteConfig?.dictionary?["header_text"] as? String } catch { // handle the error } ``` ```swift showLineNumbers Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID") { result in let paywall = try? result.get() let headerText = paywall?.remoteConfig?.dictionary?["header_text"] as? String } ``` At this point, once you've received all the necessary values, it's time to render and assemble them into a visually appealing page. Ensure that the design accommodates various mobile phone screens and orientations, providing a seamless and user-friendly experience across different devices. :::warning Make sure to [record the paywall view event](present-remote-config-paywalls#track-paywall-view-events) as described below, allowing Adapty analytics to capture information for funnels and A/B tests. ::: After you've done with displaying the paywall, continue with setting up a purchase flow. When the user makes a purchase, simply call `.makePurchase()` with the product from your paywall. For details on the`.makePurchase()` method, read [Making purchases](making-purchases). We recommend [creating a backup paywall called a fallback paywall](fallback-paywalls). This backup will display to the user when there's no internet connection or cache available, ensuring a smooth experience even in these situations. ## Track paywall view events Adapty assists you in measuring the performance of your paywalls. While we gather data on purchases automatically, logging paywall views needs your input because only you know when a customer sees a paywall. To log a paywall view event, simply call `.logShowPaywall(paywall)`, and it will be reflected in your paywall metrics in funnels and A/B tests. :::important Calling `.logShowPaywall(paywall)` is not needed if you are displaying paywalls created in the [paywall builder](adapty-paywall-builder.md). ::: ```swift showLineNumbers Adapty.logShowPaywall(paywall) ``` Request parameters: | Parameter | Presence | Description | | :---------- | :------- |:-----------------------------------------------------------------------------------------| | **paywall** | required | An [`AdaptyPaywall`](https://swift.adapty.io/documentation/adapty/adaptypaywall) object. | --- # File: making-purchases --- --- title: "Make purchases in mobile app in iOS SDK" description: "Guide on handling in-app purchases and subscriptions using Adapty." --- Displaying paywalls within your mobile app is an essential step in offering users access to premium content or services. However, simply presenting these paywalls is enough to support purchases only if you use [Paywall Builder](adapty-paywall-builder) to customize your paywalls. If you don't use the Paywall Builder, you must use a separate method called `.makePurchase()` to complete a purchase and unlock the desired content. This method serves as the gateway for users to engage with the paywalls and proceed with their desired transactions. If your paywall has an active promotional offer for the product a user is trying to buy, Adapty will automatically apply it at the time of purchase. :::warning Keep in mind that the introductory offer will be applied automatically only if you use the paywalls set up using the Paywall Builder. In other cases, you'll need to [verify the user's eligibility for an introductory offer on iOS](fetch-paywalls-and-products#check-intro-offer-eligibility-on-ios). Skipping this step may result in your app being rejected during release. Moreover, it could lead to charging the full price to users who are eligible for an introductory offer. ::: Make sure you've [done the initial configuration](quickstart) without skipping a single step. Without it, we can't validate purchases. ## Make purchase :::note **Using [Paywall Builder](adapty-paywall-builder)?** Purchases are processed automatically—you can skip this step. **Looking for step-by-step guidance?** Check out the [quickstart guide](ios-implement-paywalls-manually) for end-to-end implementation instructions with full context. ::: ```swift showLineNumbers do { let purchaseResult = try await Adapty.makePurchase(product: product) switch purchaseResult { case .userCancelled: // Handle the case where the user canceled the purchase case .pending: // Handle deferred purchases (e.g., the user will pay offline with cash) case let .success(profile, transaction): if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // Grant access to the paid features } } } catch { // Handle the error } ``` ```swift showLineNumbers Adapty.makePurchase(product: product) { result in switch result { case let .success(purchaseResult): switch purchaseResult { case .userCancelled: // Handle the case where the user canceled the purchase case .pending: // Handle deferred purchases (e.g., the user will pay offline with cash) case let .success(profile, transaction): if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // Grant access to the paid features } } case let .failure(error): // Handle the error } } ``` Request parameters: | Parameter | Presence | Description | | :---------- | :------- | :-------------------------------------------------------------------------------------------------- | | **Product** | required | An [`AdaptyPaywallProduct`](https://swift.adapty.io/documentation/adapty/adaptypaywallproduct) object retrieved from the paywall. | Response parameters: | Parameter | Description | |---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Profile** |

If the request has been successful, the response contains this object. An [AdaptyProfile](https://swift.adapty.io/documentation/adapty/adaptyprofile) object provides comprehensive information about a user's access levels, subscriptions, and non-subscription purchases within the app.

Check the access level status to ascertain whether the user has the required access to the app.

| :::warning **Note:** if you're still on Apple's StoreKit version lower than v2.0 and Adapty SDK version lower than v.2.9.0, you need to provide [Apple App Store shared secret](app-store-connection-configuration#step-4-enter-app-store-shared-secret) instead. This method is currently deprecated by Apple. ::: ## In-app purchases from the App Store When a user initiates a purchase in the App Store and the transaction carries over to your app, you have two options: - **Process the transaction immediately:** Return `true` in `shouldAddStorePayment`. This will trigger the Apple purchase system screen right away. - **Store the product object for later processing:** Return `false` in `shouldAddStorePayment`, then call `makePurchase` with the stored product later. This may be useful if you need to show something custom to your user before triggering a purchase. Here’s the complete snippet: ```swift showLineNumbers title="Swift" final class YourAdaptyDelegateImplementation: AdaptyDelegate { nonisolated func shouldAddStorePayment(for product: AdaptyDeferredProduct) -> Bool { // 1a. // Return `true` to continue the transaction in your app. The Apple purchase system screen will show automatically. // 1b. // Store the product object and return `false` to defer or cancel the transaction. false } // 2. Continue the deferred purchase later on by passing the product to `makePurchase` when the timing is appropriate func continueDeferredPurchase() async { let storedProduct: AdaptyDeferredProduct = // get the product object from 1b. do { try await Adapty.makePurchase(product: storedProduct) } catch { // handle the error } } } ``` ## Redeem Offer Code in iOS Since iOS 14.0, your users can redeem Offer Codes. Code redemption means using a special code, like a promotional or gift card code, to get free access to content or features in an app or on the App Store. To enable users to redeem offer codes, you can display the offer code redemption sheet by using the appropriate SDK method: ```swift showLineNumbers Adapty.presentCodeRedemptionSheet() ``` :::danger Based on our observations, the Offer Code Redemption sheet in some apps may not work reliably. We recommend redirecting the user directly to the App Store. In order to do this, you need to open the url of the following format: `https://apps.apple.com/redeem?ctx=offercodes&id={apple_app_id}&code={code}` ::: --- # File: restore-purchase --- --- title: "Restore purchases in mobile app in iOS SDK" description: "Learn how to restore purchases in Adapty to ensure seamless user experience." --- Restoring Purchases is a feature that allows users to regain access to previously purchased content, such as subscriptions or in-app purchases, without being charged again. This feature is especially useful for users who may have uninstalled and reinstalled the app or switched to a new device and want to access their previously purchased content without paying again. :::note In paywalls built with [Paywall Builder](adapty-paywall-builder), purchases are restored automatically without additional code from you. If that's your case — you can skip this step. ::: To restore a purchase if you do not use the [Paywall Builder](adapty-paywall-builder) to customize the paywall, call `.restorePurchases()` method: ```swift showLineNumbers do { let profile = try await Adapty.restorePurchases() if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // successful access restore } } catch { // handle the error } ``` ```swift showLineNumbers Adapty.restorePurchases { [weak self] result in switch result { case let .success(profile): if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // successful access restore } case let .failure(error): // handle the error } } ``` Response parameters: | Parameter | Description | |---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Profile** |

An [`AdaptyProfile`](https://swift.adapty.io/documentation/adapty/adaptyprofile) object. This model contains info about access levels, subscriptions, and non-subscription purchases.

Сheck the **access level status** to determine whether the user has access to the app.

| :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: --- # File: ios-transaction-management --- --- title: "Advanced transaction management in iOS SDK" description: "Finish transactions manually in your iOS app with Adapty SDK." --- :::note Advanced transaction management is supported in the Adapty iOS SDK starting from version 3.12. ::: Advanced transaction management in Adapty gives you more control over how transactions are handled, verified, and finished. Advanced transaction management introduces three optional features that work together: | Feature | Purpose | |-------------------------------------------------------------|----------| | [`appAccountToken`](#assign-appaccounttoken) | Links Apple transactions to your internal user ID | | [`jwsTransaction`](#access-the-jws-representation) | Provides Apple’s signed transaction payload for validation | | [Manual finishing](#control-transaction-finishing-behavior) | Lets you finish transactions only after your backend confirms success | Together, these tools let you build robust custom validation flows while Adapty continues syncing transactions with its backend. :::important Most apps don’t need this. By default, Adapty automatically validates and finishes StoreKit transactions. Use this guide only if you run your own backend validation or want to fully control the purchase lifecycle. ::: ## Assign `appAccountToken` [`appAccountToken`](https://developer.apple.com/documentation/storekit/product/purchaseoption/appaccounttoken(_:)) is a **UUID** that lets you link App Store transactions to your internal user identity. StoreKit associates this token with every transaction, so your backend can match App Store data to your users. Use a stable UUID generated per user and reuse it for the same account across devices. This ensures that purchases and App Store notifications stay correctly linked. You can set the token in two ways – during the SDK activation or when identifying the user. :::important You must always pass `appAccountToken` together with `customerUserId`. If you pass only the token, it will not be included in the transaction. ::: ```swift showLineNumbers // During configuration: let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(customerUserId: "YOUR_USER_ID", withAppAccountToken: UUID()) do { try await Adapty.activate(with: configurationBuilder.build()) } catch { // handle the error } // Or when identifying a user: do { try await Adapty.identify("YOUR_USER_ID", withAppAccountToken: UUID()) } catch { // handle the error } ``` ```swift showLineNumbers // During configuration: let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(customerUserId: "YOUR_USER_ID", withAppAccountToken: ) Adapty.activate(with: configurationBuilder.build()) { error in // handle the error } // Or when identifying a user: Adapty.identify("YOUR_USER_ID", withAppAccountToken: ) { error in if let error { // handle the error } } ``` ## Access the JWS representation When you make a purchase, the result includes Apple’s transaction in [JWS Compact Serialization format](https://developer.apple.com/documentation/storekit/verificationresult/jwsrepresentation-21vgo). You can forward this value to your backend for independent validation or logging. ```swift let result = try await Adapty.makePurchase(product: paywallProduct) let jwsRepresentation = result.jwsTransaction ``` ## Control transaction finishing behavior By default, Adapty automatically finishes StoreKit transactions after validation. If you need to delay finishing until your backend confirms success, set the finishing behavior to manual. In this mode: - Adapty still validates purchases and syncs them with its backend. - Transactions remain unfinished until you explicitly call `finish()`. ```swift var configBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_API_KEY") .with(transactionFinishBehavior: .manual) try await Adapty.activate(with: configBuilder.build()) ``` When using manual transaction finishing, you need to implement the `onUnfinishedTransaction` delegate method to handle unfinished transactions: ```swift showLineNumbers title="Swift" extension YourApp: AdaptyDelegate { func onUnfinishedTransaction(_ transaction: AdaptyUnfinishedTransaction) async { // Perform your custom validation logic here // When ready, finish the transaction await transaction.finish() } } ``` To get all current unfinished transactions, use the `getUnfinishedTransactions()` method: ```swift let unfinishedTransactions = try await Adapty.getUnfinishedTransactions() ``` --- # File: implement-observer-mode --- --- title: "Implement Observer mode in iOS SDK" description: "Implement observer mode in Adapty to track user subscription events in iOS SDK." --- If you already have your own purchase infrastructure and aren't ready to fully switch to Adapty, you can explore [Observer mode](observer-vs-full-mode). In its basic form, Observer Mode offers advanced analytics and seamless integration with attribution and analytics systems. If this meets your needs, you only need to: 1. Turn it on when configuring the Adapty SDK by setting the `observerMode` parameter to `true`. 2. [Report transactions](report-transactions-observer-mode) from your existing purchase infrastructure to Adapty. If you also need paywalls and A/B testing, additional setup is required, as described below. ## Observer mode setup Turn on the Observer mode if you handle purchases and subscription status yourself and use Adapty for sending subscription events and analytics. :::important When running in Observer mode, Adapty SDK won't close any transactions, so make sure you're handling it. ::: ```swift showLineNumbers @main struct YourApp: App { init() { // Configure Adapty SDK let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") // Get from Adapty dashboard .with(observerMode: true) let config = configurationBuilder.build() // Activate Adapty SDK asynchronously Task { do { try await Adapty.activate(with: configurationBuilder) } catch { // Handle error appropriately for your app print("Adapty activation failed: ", error) } } var body: some Scene { WindowGroup { // Your content view } } } } ``` ```swift showLineNumbers Task { do { let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") // Get from Adapty dashboard .with(observerMode: true) let config = configurationBuilder.build() try await Adapty.activate(with: config) } catch { // Handle error appropriately for your app print("Adapty activation failed: ", error) } } ``` Parameters: | Parameter | Description | | --------------------------- | ------------------------------------------------------------ | | observerMode | A boolean value that controls [Observer mode](observer-vs-full-mode). The default value is `false`. | ## Using Adapty paywalls in Observer Mode If you also want to use Adapty's paywalls and A/B testing features, you can — but it requires some extra setup in Observer mode. Here's what you'll need to do in addition to the steps above: 1. Display paywalls as usual for [remote config paywalls](present-remote-config-paywalls). For Paywall Builder paywalls, follow the specific setup guides for [iOS](ios-present-paywall-builder-paywalls-in-observer-mode). 3. [Associate paywalls](report-transactions-observer-mode) with purchase transactions. --- # File: report-transactions-observer-mode --- --- title: "Report transactions in Observer Mode in iOS SDK" description: "Report purchase transactions in Adapty Observer Mode for user insights and revenue tracking in iOS SDK." --- In Observer mode, the Adapty SDK can't track purchases made through your existing purchase system on its own. You need to report transactions from your app store. It's crucial to set this up **before** releasing your app to avoid errors in analytics. Use `reportTransaction` to explicitly report each transaction for Adapty to recognize it. :::warning **Don't skip transaction reporting!** If you don't call `reportTransaction`, Adapty won't recognize the transaction, it won't appear in analytics, and it won't be sent to integrations. ::: If you use Adapty paywalls, include the `variationId` when reporting a transaction. This links the purchase to the paywall that triggered it, ensuring accurate paywall analytics. ```swift showLineNumbers do { // every time when calling transasction.finish() try await Adapty.reportTransaction(transaction, withVariationId: ) } catch { // handle the error } ``` Parameters: | Parameter | Presence | Description | | --------------- | -------- | ------------------------------------------------------------ | | **transaction** | required |
  • For StoreKit 1: SKPaymentTransaction.
  • For StoreKit 2: Transaction.
| | **variationId** | optional | The unique ID of the paywall variation. Retrieve it from the `variationId` property of the [AdaptyPaywall](https://swift.adapty.io/documentation/adapty/adaptypaywall) object. |
In Observer mode, the Adapty SDK can't track purchases made through your existing purchase system on its own. You need to report transactions from your app store or restore them. It's crucial to set this up **before** releasing your app to avoid errors in analytics. Use `reportTransaction` to send the transaction data to Adapty. :::warning **Don't skip transaction reporting!** If you don't call `reportTransaction`, Adapty won't recognize the transaction, it won't appear in analytics, and it won't be sent to integrations. ::: If you use Adapty paywalls, include the `withVariationId` when reporting a transaction. This links the purchase to the paywall that triggered it, ensuring accurate paywall analytics. ```swift showLineNumbers do { // every time when calling transasction.finish() try await Adapty.reportTransaction(transaction, withVariationId: ) } catch { // handle the error } ``` Parameters: | Parameter | Presence | Description | | --------------- | -------- | ------------------------------------------------------------ | | **transaction** | required |
  • For StoreKit 1: SKPaymentTransaction.
  • For StoreKit 2: Transaction.
| | **variationId** | optional | The unique ID of the paywall variation. Retrieve it from the `variationId` property of the [AdaptyPaywall](https://swift.adapty.io/documentation/adapty/adaptypaywall) object. |
**Reporting transactions** - Versions up to 3.1.x automatically listen for transactions in the App Store, so manual reporting is not required. - Version 3.2 does not support Observer Mode. **Associating paywalls to transactions** Adapty SDK cannot determine the source of purchases, as you are the one processing them. Therefore, if you intend to use paywalls and/or A/B tests in Observer mode, you need to associate the transaction coming from your app store with the corresponding paywall in your mobile app code. This is important to get right before releasing your app, otherwise, it will lead to errors in analytics. ```swift let variationId = paywall.variationId // There are two overloads: for StoreKit 1 and StoreKit 2 Adapty.setVariationId(variationId, forPurchasedTransaction: transactionId) { error in if error == nil { // successful binding } } ``` Request parameters: | Parameter | Presence | Description | | ------------- | -------- | ------------------------------------------------------------ | | variationId | required | The string identifier of the variation. You can get it using `variationId` property of the [AdaptyPaywall](https://swift.adapty.io/documentation/adapty/adaptypaywall) object. | | transactionId | required |

For StoreKit 1: an [SKPaymentTransaction](https://developer.apple.com/documentation/storekit/skpaymenttransaction) object.

For StoreKit 2: [Transaction](https://developer.apple.com/documentation/storekit/transaction) object.

|
--- # File: ios-present-paywall-builder-paywalls-in-observer-mode --- --- title: "Present Paywall Builder paywalls in Observer mode in iOS SDK" description: "Learn how to present PB paywalls in observer mode for better insights." --- If you've customized a paywall using the Paywall Builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. :::warning This section refers to [Observer mode](observer-vs-full-mode) only. If you do not work in the Observer mode, refer to the [iOS - Present Paywall Builder paywalls](ios-present-paywalls). :::
Before you start presenting paywalls (Click to Expand) 1. Set up initial integration of Adapty [with the Google Play](initial-android) and [with the App Store](initial_ios). 2. Install and configure Adapty SDK. Make sure to set the `observerMode` parameter to `true`. Refer to our framework-specific instructions for [iOS](sdk-installation-ios#configure-adapty-sdk). 3. [Create products](create-product) in the Adapty Dashboard. 4. [Configure paywalls, assign products to them](create-paywall), and customize them using Paywall Builder in the Adapty Dashboard. 5. [Create placements and assign your paywalls to them](create-placement) in the Adapty Dashboard. 6. [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls) in your mobile app code.

1. Implement the `AdaptyObserverModeResolver` object: ```swift showLineNumbers title="Swift" func observerMode(didInitiatePurchase product: AdaptyPaywallProduct, onStartPurchase: @escaping () -> Void, onFinishPurchase: @escaping () -> Void) { // use the product object to handle the purchase // use the onStartPurchase and onFinishPurchase callbacks to notify AdaptyUI about the process of the purchase } func observerModeDidInitiateRestorePurchases(onStartRestore: @escaping () -> Void, onFinishRestore: @escaping () -> Void) { // use the onStartRestore and onFinishRestore callbacks to notify AdaptyUI about the process of the restore } ``` The `observerMode(didInitiatePurchase:onStartPurchase:onFinishPurchase:)` event will inform you that the user has initiated a purchase. You can trigger your custom purchase flow in response to this callback. The `observerModeDidInitiateRestorePurchases(onStartRestore:onFinishRestore:)` event will inform you that the user has initiated a restore. You can trigger your custom restore flow in response to this callback. Also, remember to invoke the following callbacks to notify AdaptyUI about the process of the purchase or restore. This is necessary for proper paywall behavior, such as showing the loader, among other things: | Callback | Description | | :----------------- | :------------------------------------------------------------------------------- | | onStartPurchase() | The callback should be invoked to notify AdaptyUI that the purchase is started. | | onFinishPurchase() | The callback should be invoked to notify AdaptyUI that the purchase is finished. | | onStartRestore() | The callback should be invoked to notify AdaptyUI that the restore is started. | | onFinishRestore() | The callback should be invoked to notify AdaptyUI that the restore is finished. | 2. Create a paywall configuration object: ```swift showLineNumbers title="Swift" do { let paywallConfiguration = try AdaptyUI.getPaywallConfiguration( forPaywall: , observerModeResolver: ) } catch { // handle the error } ``` Request parameters: | Parameter | Presence | Description | | :----------------------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Paywall** | required | An `AdaptyPaywall` object to obtain a controller for the desired paywall. | | **ObserverModeResolver** | required | The `AdaptyObserverModeResolver` object you've implemented in the previous step | 3. Initialize the visual paywall you want to display by using the `.paywallController(for:products:viewConfiguration:delegate:)` method: ```swift showLineNumbers title="Swift" import Adapty import AdaptyUI let visualPaywall = AdaptyUI.paywallController( with: , delegate: ) ``` Request parameters: | Parameter | Presence | Description | | :----------------------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Paywall Configuration** | required | An `AdaptyUI.PaywallConfiguration` object containing visual details of the paywall. Use the `AdaptyUI.getPaywallConfiguration(forPaywall:locale:)` method. Refer to [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls) topic for more details. | | **Delegate** | required | An `AdaptyPaywallControllerDelegate` to listen to paywall events. Refer to [Handling paywall events](ios-handling-events) topic for more details. | Returns: | Object | Description | | :---------------------- | :--------------------------------------------------- | | AdaptyPaywallController | An object, representing the requested paywall screen | After the object has been successfully created, you can display it like so: ```swift showLineNumbers title="Swift" present(visualPaywall, animated: true) ``` :::warning Don't forget to [Associate paywalls to purchase transactions](report-transactions-observer-mode). Otherwise, Adapty will not determine the source paywall of the purchase. ::: In order to display the visual paywall on the device screen, use the `.paywall` modifier in SwiftUI: ```swift showLineNumbers title="SwiftUI" @State var paywallPresented = false var body: some View { Text("Hello, AdaptyUI!") .paywall( isPresented: $paywallPresented, paywallConfiguration: , didPerformAction: { action in switch action { case .close: paywallPresented = false default: // Handle other actions break } }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, didFailRendering: { error in paywallPresented = false } ) } ``` Request parameters: | Parameter | Presence | Description | | :----------------------- | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Paywall Configuration** | required | An `AdaptyUI.PaywallConfiguration` object containing visual details of the paywall. Use the `AdaptyUI.getPaywallConfiguration(forPaywall:locale:)` method. Refer to [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls) topic for more details. | | **Products** | optional | Provide an array of `AdaptyPaywallProducts` to optimize the display timing of products on the screen. If `nil` is passed, AdaptyUI will automatically fetch the necessary products. | | **TagResolver** | optional | Define a dictionary of custom tags and their resolved values. Custom tags serve as placeholders in the paywall content, dynamically replaced with specific strings for personalized content within the paywall. Refer to [Custom tags in Paywall Builder](custom-tags-in-paywall-builder) topic for more details. | | **ObserverModeResolver** | optional | The `AdaptyObserverModeResolver` object you've implemented in the previous step | Closure parameters: | Closure parameter | Description | | :------------------- | :-------------------------------------------------------------------------------- | | **didFinishRestore** | If Adapty.restorePurchases() succeeds, this callback will be invoked. | | **didFailRestore** | If Adapty.restorePurchases() fails, this callback will be invoked. | | **didFailRendering** | If an error occurs during the interface rendering, this callback will be invoked. | Refer to the [iOS - Handling events](ios-handling-events) topic for other closure parameters. :::warning Don't forget to [Associate paywalls to purchase transactions](report-transactions-observer-mode). Otherwise, Adapty will not determine the source paywall of the purchase. :::
Before you start presenting paywalls (Click to Expand) 1. Set up initial integration of Adapty [with the Google Play](initial-android) and [with the App Store](initial_ios). 1. Install and configure Adapty SDK. Make sure to set the `observerMode` parameter to `true`. Refer to our framework-specific instructions for [iOS](sdk-installation-ios#configure-adapty-sdk), [React Native](sdk-installation-reactnative#configure-adapty-sdks), [Flutter](sdk-installation-flutter#configure-adapty-sdk), and [Unity](sdk-installation-unity#configure-adapty-sdk). 2. [Create products](create-product) in the Adapty Dashboard. 3. [Configure paywalls, assign products to them](create-paywall), and customize them using Paywall Builder in the Adapty Dashboard. 4. [Create placements and assign your paywalls to them](create-placement) in the Adapty Dashboard. 5. [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls) in your mobile app code.

1. Implement the `AdaptyObserverModeDelegate` object: ```swift showLineNumbers title="Swift" func paywallController(_ controller: AdaptyPaywallController, didInitiatePurchase product: AdaptyPaywallProduct, onStartPurchase: @escaping () -> Void, onFinishPurchase: @escaping () -> Void) { // use the product object to handle the purchase // use the onStartPurchase and onFinishPurchase callbacks to notify AdaptyUI about the process of the purchase } ``` The `paywallController(_:didInitiatePurchase:onStartPurchase:onFinishPurchase:)` event will inform you that the user has initiated a purchase. You can trigger your custom purchase flow in response to this event. Also, remember to invoke the following callbacks to notify AdaptyUI about the process of the purchase. This is necessary for proper paywall behavior, such as showing the loader, among other things: | Callback | Description | | :--------------- | :------------------------------------------------------------------------------- | | onStartPurchase | The callback should be invoked to notify AdaptyUI that the purchase is started. | | onFinishPurchase | The callback should be invoked to notify AdaptyUI that the purchase is finished. | 2. Initialize the visual paywall you want to display by using the `.paywallController(for:products:viewConfiguration:delegate:observerModeDelegate:)` method: ```swift showLineNumbers title="Swift" import Adapty import AdaptyUI let visualPaywall = AdaptyUI.paywallController( for: , products: , viewConfiguration: , delegate: observerModeDelegate: ) ``` Request parameters: | Parameter | Presence | Description | | :----------------------- | :------- | :----------------------------------------------------------- | | **Paywall** | required | An `AdaptyPaywall` object to obtain a controller for the desired paywall. | | **Products** | optional | Provide an array of `AdaptyPaywallProducts` to optimize the display timing of products on the screen. If `nil` is passed, AdaptyUI will automatically fetch the necessary products. | | **ViewConfiguration** | required | An `AdaptyUI.LocalizedViewConfiguration` object containing visual details of the paywall. Use the `AdaptyUI.getViewConfiguration(paywall:locale:)` method. Refer to [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls) topic for more details. | | **Delegate** | required | An `AdaptyPaywallControllerDelegate` to listen to paywall events. Refer to [Handling paywall events](ios-handling-events) topic for more details. | | **ObserverModeDelegate** | required | The `AdaptyObserverModeDelegate` object you've implemented in the previous step | | **TagResolver** | optional | Define a dictionary of custom tags and their resolved values. Custom tags serve as placeholders in the paywall content, dynamically replaced with specific strings for personalized content within the paywall. Refer to [Custom tags in Paywall Builder](custom-tags-in-paywall-builder) topic for more details. | Returns: | Object | Description | | :---------------------- | :--------------------------------------------------- | | AdaptyPaywallController | An object, representing the requested paywall screen | After the object has been successfully created, you can display it like so: ```swift showLineNumbers title="Swift" present(visualPaywall, animated: true) ``` :::warning Don't forget to [Associate paywalls to purchase transactions](report-transactions-observer-mode). Otherwise, Adapty will not determine the source paywall of the purchase. ::: In order to display the visual paywall on the device screen, use the `.paywall` modifier in SwiftUI: ```swift showLineNumbers title="SwiftUI" @State var paywallPresented = false var body: some View { Text("Hello, AdaptyUI!") .paywall( isPresented: $paywallPresented, paywall: , configuration: , didPerformAction: { action in switch action { case .close: paywallPresented = false default: // Handle other actions break } }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, didFailRendering: { error in paywallPresented = false }, observerModeDidInitiatePurchase: { product, onStartPurchase, onFinishPurchase in // use the product object to handle the purchase // use the onStartPurchase and onFinishPurchase callbacks to notify AdaptyUI about the process of the purchase }, ) } ``` Request parameters: | Parameter | Presence | Description | | :---------------- | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Paywall** | required | An `AdaptyPaywall` object to obtain a controller for the desired paywall. | | **Product** | optional | Provide an array of `AdaptyPaywallProducts` to optimize the display timing of products on the screen. If `nil` is passed, AdaptyUI will automatically fetch the necessary products. | | **Configuration** | required | An `AdaptyUI.LocalizedViewConfiguration` object containing visual details of the paywall. Use the `AdaptyUI.getViewConfiguration(paywall:locale:)` method. Refer to [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls) topic for more details. | | **TagResolver** | optional | Define a dictionary of custom tags and their resolved values. Custom tags serve as placeholders in the paywall content, dynamically replaced with specific strings for personalized content within the paywall. Refer to [Custom tags in paywall builder](custom-tags-in-paywall-builder) topic for more details. | Closure parameters: | Closure parameter | Description | | :---------------------------------- | :-------------------------------------------------------------------------------- | | **didFinishRestore** | If Adapty.restorePurchases() succeeds, this callback will be invoked. | | **didFailRestore** | If Adapty.restorePurchases() fails, this callback will be invoked. | | **didFailRendering** | If an error occurs during the interface rendering, this callback will be invoked. | | **observerModeDidInitiatePurchase** | This callback is invoked when a user initiates a purchase. | Refer to the [iOS - Handling events](ios-handling-events) topic for other closure parameters. :::warning Don't forget to [Associate paywalls to purchase transactions](report-transactions-observer-mode). Otherwise, Adapty will not determine the source paywall of the purchase. :::
--- # File: ios-troubleshoot-purchases --- --- title: "Troubleshoot purchases in iOS SDK" description: "Troubleshoot purchases in iOS SDK" --- This guide helps you resolve common issues when implementing purchases manually in the iOS SDK. ## AdaptyError.cantMakePayments in observer mode **Issue**: You're getting `AdaptyError.cantMakePayments` when using `makePurchase` in observer mode. **Reason**: In observer mode, you should handle purchases on your side, not use Adapty's `makePurchase` method. **Solution**: If you use `makePurchase` for purchases, turn off the observer mode. You need either to use `makePurchase` or handle purchases on your side in the observer mode. See [Implement Observer mode](implement-observer-mode) for more details. ## Not found makePurchasesCompletionHandlers **Issue**: You're encountering issues with `makePurchasesCompletionHandlers` not being found. **Reason**: This is typically related to sandbox testing issues. **Solution**: Create a new sandbox user and try again. This often resolves sandbox-related purchase completion handler issues. ## Other issues **Issue**: You're experiencing other purchase-related problems not covered above. **Solution**: Migrate the SDK to the latest version using the [migration guides](ios-sdk-migration-guides) if needed. Many issues are resolved in newer SDK versions. --- # File: identifying-users --- --- title: "Identify users in iOS SDK" description: "Identify users in Adapty to improve personalized subscription experiences." --- Adapty creates an internal profile ID for every user. However, if you have your own authentication system, you should set your own Customer User ID. You can find users by their Customer User ID in the [Profiles](profiles-crm) section and use it in the [server-side API](getting-started-with-server-side-api), which will be sent to all integrations. ## Set customer user ID on configuration If you have a user ID during configuration, just pass it as `customerUserId` parameter to `.activate()` method: ```swift showLineNumbers // In your AppDelegate class: let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(customerUserId: "YOUR_USER_ID") do { try await Adapty.activate(with: configurationBuilder.build()) } catch { // handle the error } ``` ```swift showLineNumbers // In your AppDelegate class: let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(customerUserId: "YOUR_USER_ID") Adapty.activate(with: configurationBuilder.build()) { error in // handle the error } ``` :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ## Set customer user ID after configuration If you don't have a user ID in the SDK configuration, you can set it later at any time with the `.identify()` method. The most common cases for using this method are after registration or authorization, when the user switches from being an anonymous user to an authenticated user. ```swift showLineNumbers do { try await Adapty.identify("YOUR_USER_ID") } catch { // handle the error } ``` ```swift showLineNumbers Adapty.identify("YOUR_USER_ID") { error in if let error { // handle the error } } ``` Request parameters: - **Customer User ID** (required): a string user identifier. :::warning Resubmitting of significant user data In some cases, such as when a user logs into their account again, Adapty's servers already have information about that user. In these scenarios, the Adapty SDK will automatically switch to work with the new user. If you passed any data to the anonymous user, such as custom attributes or attributions from third-party networks, you should resubmit that data for the identified user. It's also important to note that you should re-request all paywalls and products after identifying the user, as the new user's data may be different. ::: ## Logging out and logging in You can logout the user anytime by calling `.logout()` method: ```swift showLineNumbers do { try await Adapty.logout() } catch { // handle the error } ``` ```swift showLineNumbers Adapty.logout { error in if error == nil { // successful logout } } ``` You can then login the user using `.identify()` method. ## Set appAccountToken The [`appAccountToken`](https://developer.apple.com/documentation/storekit/product/purchaseoption/appaccounttoken(_:)) is a UUID that helps Apple's StoreKit 2 identify users across app installations and devices. Starting from the Adapty iOS SDK 3.10.2, you can pass the `appAccountToken` when configuring the SDK or when identifying a user: ```swift showLineNumbers // During configuration: let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(customerUserId: "YOUR_USER_ID", withAppAccountToken: UUID()) do { try await Adapty.activate(with: configurationBuilder.build()) } catch { // handle the error } // Or when identifying a user: do { try await Adapty.identify("YOUR_USER_ID", withAppAccountToken: UUID()) } catch { // handle the error } ``` ```swift showLineNumbers // During configuration: let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(customerUserId: "YOUR_USER_ID", withAppAccountToken: UUID()) Adapty.activate(with: configurationBuilder.build()) { error in // handle the error } // Or when identifying a user: Adapty.identify("YOUR_USER_ID", withAppAccountToken: UUID()) { error in if let error { // handle the error } } ``` You can then login the user using `.identify()` method. --- # File: setting-user-attributes --- --- title: "Set user attributes in iOS SDK" description: "Learn how to set user attributes in Adapty to enable better audience segmentation." --- You can set optional attributes such as email, phone number, etc, to the user of your app. You can then use attributes to create user [segments](segments) or just view them in CRM. ### Setting user attributes To set user attributes, call `.updateProfile()` method: ```swift showLineNumbers let builder = AdaptyProfileParameters.Builder() .with(email: "email@email.com") .with(phoneNumber: "+18888888888") .with(firstName: "John") .with(lastName: "Appleseed") .with(gender: .other) .with(birthday: Date()) do { try await Adapty.updateProfile(params: builder.build()) } catch { // handle the error } ``` ```swift showLineNumbers let builder = AdaptyProfileParameters.Builder() .with(email: "email@email.com") .with(phoneNumber: "+18888888888") .with(firstName: "John") .with(lastName: "Appleseed") .with(gender: .other) .with(birthday: Date()) Adapty.updateProfile(params: builder.build()) { error in if error != nil { // handle the error } } ``` Please note that the attributes that you've previously set with the `updateProfile` method won't be reset. :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ### The allowed keys list The allowed keys `` of `AdaptyProfileParameters.Builder` and the values `` are listed below: | Key | Value | |---|-----| |

email

phoneNumber

firstName

lastName

| String | | gender | Enum, allowed values are: `female`, `male`, `other` | | birthday | Date | ### Custom user attributes You can set your own custom attributes. These are usually related to your app usage. For example, for fitness applications, they might be the number of exercises per week, for language learning app user's knowledge level, and so on. You can use them in segments to create targeted paywalls and offers, and you can also use them in analytics to figure out which product metrics affect the revenue most. ```swift showLineNumbers do { builder = try builder.with(customAttribute: "value1", forKey: "key1") } catch { // handle key/value validation error } ``` To remove existing key, use `.withRemoved(customAttributeForKey:)` method: ```swift showLineNumbers do { builder = try builder.withRemoved(customAttributeForKey: "key2") } catch { // handle error } ``` Sometimes you need to figure out what custom attributes have already been installed before. To do this, use the `customAttributes` field of the `AdaptyProfile` object. :::warning Keep in mind that the value of `customAttributes` may be out of date since the user attributes can be sent from different devices at any time so the attributes on the server might have been changed after the last sync. ::: ### Limits - Up to 30 custom attributes per user - Key names are up to 30 characters long. The key name can include alphanumeric characters and any of the following: `_` `-` `.` - Value can be a string or float with no more than 50 characters. --- # File: subscription-status --- --- title: "Check subscription status in iOS SDK" description: "Track and manage user subscription status in Adapty for improved customer retention." --- With Adapty, keeping track of subscription status is made easy. You don't have to manually insert product IDs into your code. Instead, you can effortlessly confirm a user's subscription status by checking for an active [access level](access-level). Before you start checking subscription status, set up [App Store Server Notifications](enable-app-store-server-notifications). ## Access level and the AdaptyProfile object Access levels are properties of the [AdaptyProfile](https://swift.adapty.io/documentation/adapty/adaptyprofile) object. We recommend retrieving the profile when your app starts, such as when you [identify a user](identifying-users#setting-customer-user-id-on-configuration) , and then updating it whenever changes occur. This way, you can use the profile object without repeatedly requesting it. To be notified of profile updates, listen for profile changes as described in the [Listening for profile updates, including access levels](subscription-status#listening-for-subscription-status-updates) section below. :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ## Retrieving the access level from the server To get the access level from the server, use the `.getProfile()` method: ```swift showLineNumbers do { let profile = try await Adapty.getProfile() if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // grant access to premium features } } catch { // handle the error } ``` ```swift showLineNumbers Adapty.getProfile { result in if let profile = try? result.get() { // check the access profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // grant access to premium features } } } ``` Response parameters: | Parameter | Description | | --------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Profile |

An [AdaptyProfile](https://swift.adapty.io/documentation/adapty/adaptyprofile) object. Generally, you have to check only the access level status of the profile to determine whether the user has premium access to the app.

The `.getProfile` method provides the most up-to-date result as it always tries to query the API. If for some reason (e.g. no internet connection), the Adapty SDK fails to retrieve information from the server, the data from the cache will be returned. It is also important to note that the Adapty SDK updates `AdaptyProfile` cache regularly, to keep this information as up-to-date as possible.

| The `.getProfile()` method provides you with the user profile from which you can get the access level status. You can have multiple access levels per app. For example, if you have a newspaper app and sell subscriptions to different topics independently, you can create access levels "sports" and "science". But most of the time, you will only need one access level, in that case, you can just use the default "premium" access level. Here is an example for checking for the default "premium" access level: ```swift showLineNumbers do { let profile = try await Adapty.getProfile() let isPremium = profile.accessLevels["premium"]?.isActive ?? false // grant access to premium features } catch { // handle the error } ``` ```swift showLineNumbers Adapty.getProfile { result in if let profile = try? result.get(), profile.accessLevels["premium"]?.isActive ?? false { // grant access to premium features } } ``` ### Listening for subscription status updates Whenever the user's subscription changes, Adapty fires an event. To receive messages from Adapty, you need to make some additional configuration: ```swift showLineNumbers Adapty.delegate = self // To receive subscription updates, extend `AdaptyDelegate` with this method: nonisolated func didLoadLatestProfile(_ profile: AdaptyProfile) { // handle any changes to subscription state } ``` Adapty also fires an event at the start of the application. In this case, the cached subscription status will be passed. ### Subscription status cache The cache implemented in the Adapty SDK stores the subscription status of the profile. This means that even if the server is unavailable, the cached data can be accessed to provide information about the profile's subscription status. However, it's important to note that direct data requests from the cache are not possible. The SDK periodically queries the server every minute to check for any updates or changes related to the profile. If there are any modifications, such as new transactions or other updates, they will be sent to the cached data in order to keep it synchronized with the server. --- # File: ios-deal-with-att --- --- title: "Deal with ATT in iOS SDK" description: "Get started with Adapty on iOS to streamline subscription setup and management." --- If your application uses AppTrackingTransparency framework and presents an app-tracking authorization request to the user, then you should send the [authorization status](https://developer.apple.com/documentation/apptrackingtransparency/attrackingmanager/authorizationstatus/) to Adapty. ```swift showLineNumbers let builder = AdaptyProfileParameters.Builder() .with(appTrackingTransparencyStatus: .authorized) do { try await Adapty.updateProfile(params: builder.build()) } catch { // handle the error } ``` ```swift showLineNumbers if #available(iOS 14, macOS 11.0, *) { let builder = AdaptyProfileParameters.Builder() .with(appTrackingTransparencyStatus: .authorized) Adapty.updateProfile(params: builder.build()) { [weak self] error in if error != nil { // handle the error } } } ``` :::warning We strongly recommend that you send this value as early as possible when it changes, only in that case the data will be sent in a timely manner to the integrations you have configured. ::: --- # File: kids-mode --- --- title: "Kids Mode in iOS SDK" description: "Easily enable Kids Mode to comply with Apple policies. No IDFA or ad data collected in iOS SDK." --- If your iOS application is intended for kids, you must follow the policies of [Apple](https://developer.apple.com/kids/). If you're using the Adapty SDK, a few simple steps will help you configure it to meet these policies and pass app store reviews. ## What's required? You need to configure the Adapty SDK to disable the collection of: - [IDFA (Identifier for Advertisers)](https://en.wikipedia.org/wiki/Identifier_for_Advertisers) - [IP address](https://www.ftc.gov/system/files/ftc_gov/pdf/p235402_coppa_application.pdf) In addition, we recommend using customer user ID carefully. User ID in format `` will be definitely treated as gathering personal data as well as using email. For Kids Mode, a best practice is to use randomized or anonymized identifiers (e.g., hashed IDs or device-generated UUIDs) to ensure compliance. ## Enabling Kids Mode ### Updates in the Adapty Dashboard In the Adapty Dashboard, you need to disable the IP address collection. To do this, go to [App settings](https://app.adapty.io/settings/general) and click **Disable IP address collection** under **Collect users' IP address**. ### Updates in your mobile app code In order to comply with policies, disable the collection of the user's IDFA and IP address. If you use Swift Package Manager, you can enable Kids Mode by selecting the **Adapty_KidsMode** module in Xcode when installing the SDK. In Xcode, go to **File** -> **Add Package Dependency...**. Note that the steps to add package dependencies may vary between Xcode versions, so refer to Xcode documentation if needed. 1. Enter the repository URL: ``` https://github.com/adaptyteam/AdaptySDK-iOS.git ``` 2. Select the version (latest stable version is recommended) and click **Add Package**. 3. In the **Choose Package Products** window, select the modules you need: - **Adapty_KidsMode** (core module) - **AdaptyUI** (optional - only if you plan to use Paywall Builder) You won't need any other packages. 4. Click **Add Package** to complete the installation. 1. Update your Podfile: - If you **don't** have a `post_install` section, add the entire code block below. - If you **do** have a `post_install` section, merge the highlighted lines into it. ```ruby showLineNumbers title="Podfile" post_install do |installer| installer.pods_project.targets.each do |target| // highlight-start if target.name == 'Adapty' target.build_configurations.each do |config| config.build_settings['OTHER_SWIFT_FLAGS'] ||= ['$(inherited)'] config.build_settings['OTHER_SWIFT_FLAGS'] << '-DADAPTY_KIDS_MODE' end end // highlight-end end end ``` 2. Run the following command to apply the changes: ```sh showLineNumbers title="Shell" pod install ``` --- # File: get-onboardings --- --- title: "Fetch onboardings and their configuration" description: "Learn how to retrieve onboardings in Adapty for." --- After [you designed the visual part for your onboarding](design-onboarding.md) with the builder in the Adapty Dashboard, you can display it in your mobile app. The first step in this process is to get the onboarding associated with the placement and its view configuration as described below. Before you start, ensure that: 1. You have installed [Adapty iOS, Android, React Native, or Flutter SDK](installation-of-adapty-sdks.md) version 3.8.0 or higher. 2. You have [created an onboarding](create-onboarding.md). 3. You have added the onboarding to a [placement](placements.md). ## Fetch onboarding When you create an [onboarding](onboardings.md) with our no-code builder, it's stored as a container with configuration that your app needs to fetch and display. This container manages the entire experience - what content appears, how it's presented, and how user interactions (like quiz answers or form inputs) are processed. The container also automatically tracks analytics events, so you don't need to implement separate view tracking. For best performance, fetch the onboarding configuration early to give images enough time to download before showing to users. To get an onboarding, use the `getOnboarding` method: ```swift showLineNumbers do { let onboarding = try await Adapty.getOnboarding(placementId: "YOUR_PLACEMENT_ID") // the requested onboarding } catch { // handle the error } ``` Parameters: | Parameter | Presence | Description | |---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the onboarding localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.

| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

Adapty SDK stores onboardings locally in two layers: regularly updated cache described above and fallback onboardings. We also use CDN to fetch onboardings faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your onboardings while ensuring reliability even in cases where internet connection is scarce.

| | **loadTimeout** | default: 5 sec |

This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.

Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.

| Response parameters: | Parameter | Description | |:----------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------| | Onboarding | An [`AdaptyOnboarding`](https://swift.adapty.io/documentation/adapty/adaptyonboarding) object with: the onboarding identifier and configuration, remote config, and several other properties. | ## Speed up onboarding fetching with default audience onboarding Typically, onboardings are fetched almost instantly, so you don't need to worry about speeding up this process. However, in cases where you have numerous audiences and onboardings, and your users have a weak internet connection, fetching a onboarding may take longer than you'd like. In such situations, you might want to display a default onboarding to ensure a smooth user experience rather than showing no onboarding at all. To address this, you can use the `getOnboardingForDefaultAudience` method, which fetches the onboarding of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the onboarding by the `getOnboarding` method, as detailed in the [Fetch Onboarding](#fetch-onboarding) section above. :::warning Consider using `getOnboarding` instead of `getOnboardingForDefaultAudience`, as the latter has important limitations: - **Compatibility issues**: May create problems when supporting multiple app versions, requiring either backward-compatible designs or accepting that older versions might display incorrectly. - **No personalization**: Only shows content for the "All Users" audience, removing targeting based on country, attribution, or custom attributes. If faster fetching outweighs these drawbacks for your use case, use `getOnboardingForDefaultAudience` as shown below. Otherwise, use `getOnboarding` as described [above](#fetch-onboarding). ::: ```swift showLineNumbers Adapty.getOnboardingForDefaultAudience(placementId: "YOUR_PLACEMENT_ID") { result in switch result { case let .success(onboarding): // the requested onboarding case let .failure(error): // handle the error } } ``` Parameters: | Parameter | Presence | Description | |---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the onboarding localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.

| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

Adapty SDK stores onboardings locally in two layers: regularly updated cache described above and fallback onboardings. We also use CDN to fetch onboardings faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your onboardings while ensuring reliability even in cases where internet connection is scarce.

| --- # File: ios-present-onboardings --- --- title: "Present onboardings in iOS SDK" description: "Discover how to present onboardings on iOS to boost conversions and revenue." --- If you've customized an onboarding using the builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such an onboarding contains both what should be shown within the onboarding and how it should be shown. Before you start, ensure that: 1. You have installed [Adapty iOS SDK](sdk-installation-ios.md) 3.8.0 or later. 2. You have [created an onboarding](create-onboarding.md). 3. You have added the onboarding to a [placement](placements.md). ## Present onboardings in Swift In order to display the visual onboarding on the device screen, do the following: 1. Get the onboarding view configuration using the `.getOnboardingConfiguration` method. 2. Initialize the visual onboarding you want to display by using the `.onboardingController` method: Request parameters: | Parameter | Presence | Description | |:-----------------------------|:---------|:------------------------------------------------------------------------------------------------------------------------------------------------------------| | **onboarding configuration** | required | An `AdaptyUI.OnboardingConfiguration` object containing all the onboarding properties. Use the `AdaptyUI.getOnboardingConfiguration` method to obtain it. | | **delegate** | required | An `AdaptyOnboardingControllerDelegate` to listen to onboarding events. | Returns: | Object | Description | |:-------------------------------|:--------------------------------------------------------| | **AdaptyOnboardingController** | An object, representing the requested onboarding screen | 3. After the object has been successfully created, you can display it on the screen of the device: ```swift showLineNumbers title="Swift" import Adapty import AdaptyUI // 0. Get an onboarding if you haven't done it yet let onboarding = try await Adapty.getOnboarding(placementId: "YOUR_PLACEMENT_ID") // 1. Obtain the onboarding view configuration: let configuration = try AdaptyUI.getOnboardingConfiguration(forOnboarding: onboarding) // 2. Create Onboarding View Controller let onboardingController = try AdaptyUI.onboardingController( with: configuration, delegate: ) // 3. Present it to the user present(onboardingController, animated: true) ``` ## Present onboardings in SwiftUI To display the visual onboarding on the device screen in SwiftUI: ```swift showLineNumbers title="SwiftUI" // 1. Obtain the onboarding view configuration: let configuration = try AdaptyUI.getOnboardingConfiguration(forOnboarding: onboarding) // 2. Display the Onboarding View within your view hierarchy AdaptyOnboardingView( configuration: configuration, placeholder: { Text("Your Placeholder View") }, onCloseAction: { action in // hide the onboarding view }, onError: { error in // handle the error } ) ``` ## Add smooth transitions between the splash screen and onboarding By default, between the splash screen and onboarding, you will see the loading screen until the onboarding is fully loaded. However, if you want to make the transition smoother, you can customize it and either extend the splash screen or display something else. To do this, define a placeholder (what exactly will be shown while the onboarding is being loaded). If you define a placeholder, the onboarding will be loaded in the background and automatically displayed once ready. ```swift showLineNumbers extension YourOnboardingManagerClass: AdaptyOnboardingControllerDelegate { func onboardingsControllerLoadingPlaceholder( _ controller: AdaptyOnboardingController ) -> UIView? { // instantiate and return the UIView which will be presented while onboarding is being loaded } } ``` ```swift showLineNumbers AdaptyOnboardingView( configuration: configuration, placeholder: { // define your placeholder view, which will be presented while onboarding is being loaded }, // the rest of the implementation ) ``` ## Customize how links open in onboardings :::important Customizing how links open in onboardings is supported starting from Adapty SDK v.3.15.1. ::: By default, links in onboardings open in an in-app browser. This provides a seamless user experience by displaying web pages within your application, allowing users to view them without switching apps. If you prefer to open links in an external browser instead, you can customize this behavior by setting the `externalUrlsPresentation` parameter to `.externalBrowser`: ```swift showLineNumbers let configuration = try AdaptyUI.getOnboardingConfiguration( forOnboarding: onboarding, externalUrlsPresentation: .externalBrowser // default – .inAppBrowser ) ``` --- # File: ios-handling-onboarding-events --- --- title: "Handle onboarding events in iOS SDK" description: "Handle onboarding-related events in iOS using Adapty." --- Before you start, ensure that: 1. You have installed [Adapty iOS SDK](sdk-installation-ios.md) 3.8.0 or later. 2. You have [created an onboarding](create-onboarding.md). 3. You have added the onboarding to a [placement](placements.md). Onboardings configured with the builder generate events your app can respond to. Learn how to respond to these events below. To control or monitor processes occurring on the onboarding screen within your mobile app, implement the `AdaptyOnboardingControllerDelegate` methods. ## Custom actions In the builder, you can add a **custom** action to a button and assign it an ID. Then, you can use this ID in your code and handle it as a custom action. For example, if a user taps a custom button, like **Login** or **Allow notifications**, the delegate method `onboardingController` will be triggered with the `.custom(id:)` case and the `actionId` parameter is the **Action ID** from the builder. You can create your own IDs, like "allowNotifications". ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, onCustomAction action: AdaptyOnboardingsCustomAction) { if action.actionId == "allowNotifications" { // Request notification permissions } } func onboardingController(_ controller: AdaptyOnboardingController, didFailWithError error: AdaptyUIError) { // Handle errors } ```
Event example (Click to expand) ```json { "actionId": "allowNotifications", "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "screensTotal": 3 } } ```
## Closing onboarding Onboarding is considered closed when a user taps a button with the **Close** action assigned. :::important Note that you need to manage what happens when a user closes the onboarding. For instance, you need to stop displaying the onboarding itself. ::: For example: ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, onCloseAction action: AdaptyOnboardingsCloseAction) { controller.dismiss(animated: true) } ```
Event example (Click to expand) ```json { "action_id": "close_button", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "final_screen", "screen_index": 3, "total_screens": 4 } } ```
## Opening a paywall :::tip Handle this event to open a paywall if you want to open it inside the onboarding. If you want to open a paywall after it is closed, there is a more straightforward way to do it – handle [`AdaptyOnboardingsCloseAction`](#closing-onboarding) and open a paywall without relying on the event data. ::: If a user clicks a button that opens a paywall, you will get a button action ID that you [set up manually](get-paid-in-onboardings.md). The most seamless way to work with paywalls in onboardings is to make the action ID equal to a paywall placement ID. This way, after the `AdaptyOnboardingsOpenPaywallAction`, you can use the placement ID to get and open the paywall right away. Note that only one view (paywall or onboarding) can be displayed on screen at a time. If you present a paywall on top of an onboarding, you cannot programmatically control the onboarding in the background. Attempting to dismiss the onboarding will close the paywall instead, leaving the onboarding visible. To avoid this, always dismiss the onboarding view before presenting the paywall. ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, onPaywallAction action: AdaptyOnboardingsOpenPaywallAction) { // Dismiss onboarding before presenting paywall controller.dismiss(animated: true) { Task { do { // Get the paywall using the placement ID from the action let paywall = try await Adapty.getPaywall(placementId: action.actionId) // Get the paywall configuration let paywallConfig = try await AdaptyUI.getPaywallConfiguration( forPaywall: paywall ) // Create and present the paywall controller let paywallController = try AdaptyUI.paywallController( with: paywallConfig, delegate: self ) // Present the paywall from the root view controller if let rootVC = UIApplication.shared.windows.first?.rootViewController { rootVC.present(paywallController, animated: true) } } catch { // Handle any errors that occur during paywall loading print("Failed to present paywall: \(error)") } } } } ```
Event example (Click to expand) ```json { "action_id": "premium_offer_1", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "pricing_screen", "screen_index": 2, "total_screens": 4 } } ```
## Finishing loading onboarding When an onboarding finishes loading, this method will be invoked: ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, didFinishLoading action: OnboardingsDidFinishLoadingAction) { // Handle loading completion } ```
Event example (Click to expand) ```json { "meta": { "onboarding_id": "onboarding_123", "screen_cid": "welcome_screen", "screen_index": 0, "total_screens": 4 } } ```
## Tracking navigation The `onAnalyticsEvent` method is called when various analytics events occur during the onboarding flow. The `event` object can be one of the following types: |Type | Description | |------------|-------------| | `onboardingStarted` | When the onboarding has been loaded | | `screenPresented` | When any screen is shown | | `screenCompleted` | When a screen is completed. Includes optional `elementId` (identifier of the completed element) and optional `reply` (response from the user). Triggered when users perform any action to exit the screen. | | `secondScreenPresented` | When the second screen is shown | | `userEmailCollected` | Triggered when the user's email is collected via the input field | | `onboardingCompleted` | Triggered when a user reaches a screen with the `final` ID. If you need this event, [assign the `final` ID to the last screen](design-onboarding.md). | | `unknown` | For any unrecognized event type. Includes `name` (the name of the unknown event) and `meta` (additional metadata) | Each event includes `meta` information containing: | Field | Description | |------------|-------------| | `onboardingId` | Unique identifier of the onboarding flow | | `screenClientId` | Identifier of the current screen | | `screenIndex` | Current screen's position in the flow | | `screensTotal` | Total number of screens in the flow | Here's an example of how you can use analytics events for tracking: ```swift func onboardingController(_ controller: AdaptyOnboardingController, onAnalyticsEvent event: AdaptyOnboardingsAnalyticsEvent) { switch event { case .onboardingStarted(let meta): // Track onboarding start trackEvent("onboarding_started", meta: meta) case .screenPresented(let meta): // Track screen presentation trackEvent("screen_presented", meta: meta) case .screenCompleted(let meta, let elementId, let reply): // Track screen completion with user response trackEvent("screen_completed", meta: meta, elementId: elementId, reply: reply) case .onboardingCompleted(let meta): // Track successful onboarding completion trackEvent("onboarding_completed", meta: meta) case .unknown(let meta, let name): // Handle unknown events trackEvent(name, meta: meta) // Handle other cases as needed } } ```
Event examples (Click to expand) ```javascript // onboardingStarted { "name": "onboarding_started", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "welcome_screen", "screen_index": 0, "total_screens": 4 } } // screenPresented { "name": "screen_presented", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "interests_screen", "screen_index": 2, "total_screens": 4 } } // screenCompleted { "name": "screen_completed", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 1, "total_screens": 4 }, "params": { "element_id": "profile_form", "reply": "success" } } // secondScreenPresented { "name": "second_screen_presented", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 1, "total_screens": 4 } } // userEmailCollected { "name": "user_email_collected", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 1, "total_screens": 4 } } // onboardingCompleted { "name": "onboarding_completed", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "final_screen", "screen_index": 3, "total_screens": 4 } } ```
--- # File: ios-onboarding-input --- --- title: "Process data from onboardings in iOS SDK" description: "Save and use data from onboardings in your iOS app with Adapty SDK." --- When your users respond to a quiz question or input their data into an input field, the `onStateUpdatedAction` method will be invoked. You can save or process the field type in your code. For example: ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, onStateUpdatedAction action: AdaptyOnboardingsStateUpdatedAction) { // Store user preferences or responses switch action.params { case .select(let params): // Handle single selection case .multiSelect(let params): // Handle multiple selections case .input(let params): // Handle text input case .datePicker(let params): // Handle date selection } } ``` The `action` object contains: | Parameter | Description | |----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `elementId` | A unique identifier for the input element. You can use it to associate questions with answers when saving them. | | `params` | The user's input data object containing type and value properties. | | `params.type` | The type of input element. Can be:
• `"select"` - Single selection from options
• `"multiSelect"` - Multiple selections from options
• `"input"` - Text input field
• `"datePicker"` - Date selection | | `params.value` | The value(s) selected or entered by the user. Structure depends on type:
• `select`: Object with `id`, `value`, `label`
• `multiSelect`: Array of objects with `id`, `value`, `label`
• `input`: Object with `type`, `value`
• `datePicker`: Object with `day`, `month`, `year` |
Saved data examples (may differ in your implementation) ```javascript // Example of a saved select action { "elementId": "preference_selector", "meta": { "onboardingId": "onboarding_123", "screenClientId": "preferences_screen", "screenIndex": 1, "screensTotal": 3 }, "params": { "type": "select", "value": { "id": "option_1", "value": "premium", "label": "Premium Plan" } } } // Example of a saved multi-select action { "elementId": "interests_selector", "meta": { "onboardingId": "onboarding_123", "screenClientId": "interests_screen", "screenIndex": 2, "screensTotal": 3 }, "params": { "type": "multiSelect", "value": [ { "id": "interest_1", "value": "sports", "label": "Sports" }, { "id": "interest_2", "value": "music", "label": "Music" } ] } } // Example of a saved input action { "elementId": "name_input", "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "screensTotal": 3 }, "params": { "type": "input", "value": { "type": "text", "value": "John Doe" } } } // Example of a saved date picker action { "elementId": "birthday_picker", "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "screensTotal": 3 }, "params": { "type": "datePicker", "value": { "day": 15, "month": 6, "year": 1990 } } } ```
## Use cases ### Enrich user profiles with data If you want to immediately link the input data with the user profile and avoid asking them twice for the same info, you need to [update the user profile](setting-user-attributes.md) with the input data when handling the action. For example, you ask users to enter their name in the text field with the `name` ID, and you want to set this field's value as user's first name. Also, you ask them to enter their email in the `email` field. In your app code, it can look like this: ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, onStateUpdatedAction action: AdaptyOnboardingsStateUpdatedAction) { // Store user preferences or responses switch action.params { case .input(let params): // Handle text input let builder = AdaptyProfileParameters.Builder() // Map elementId to appropriate profile field switch action.elementId { case "name": builder.with(firstName: params.value.value) case "email": builder.with(email: params.value.value) } do { try await Adapty.updateProfile(params: builder.build()) } catch { // handle the error } } ``` ### Customize paywalls based on answers Using quizzes in onboardings, you can also customize paywalls you show users after they complete the onboarding. For example, you can ask users about their experience with sport and show different CTAs and products to different user groups. 1. [Add a quiz](onboarding-quizzes.md) in the onboarding builder and assign meaningful IDs to its options. 2. Handle the quiz responses based on their IDs and [set custom attributes](setting-user-attributes.md) for users. ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, onStateUpdatedAction action: AdaptyOnboardingsStateUpdatedAction) { // Handle quiz responses and set custom attributes switch action.params { case .select(let params): // Handle quiz selection let builder = AdaptyProfileParameters.Builder() // Map quiz responses to custom attributes switch action.elementId { case "experience": // Set custom attribute 'experience' with the selected value (beginner, amateur, pro) try? builder.with(customAttribute: params.value.value, forKey: "experience") } do { try await Adapty.updateProfile(params: builder.build()) } catch { // handle the error } } } ``` 3. [Create segments](segments.md) for each custom attribute value. 4. Create a [placement](placements.md) and add [audiences](audience.md) for each segment you've created. 5. [Display a paywall](ios-paywalls.md) for the placement in your app code. If your onboarding has a button that opens a paywall, implement the paywall code as a [response to this button's action](ios-handling-onboarding-events#opening-a-paywall). --- # File: ios-test --- --- title: "Test & release in iOS SDK" description: "Learn how to check subscription status in your iOS app with Adapty." --- If you've already implemented the Adapty SDK in your iOS app, you'll want to test that everything is set up correctly and that purchases work as expected. This involves testing both the SDK integration and the actual purchase. ## Test your app For comprehensive testing of your in-app purchases, including sandbox testing and TestFlight validation, see our [testing guide](test-purchases-in-sandbox.md). ## Prepare for release Before submitting your app to the store, follow the [Release checklist](release-checklist) to confirm: - Store connection and server notifications are configured - Purchases complete and are reported to Adapty - Access unlocks and restores correctly - Privacy and review requirements are met --- # File: InvalidProductIdentifiers --- --- title: "Fix for Code-1000 noProductIDsFound error" description: "Resolve invalid product identifier errors when managing subscriptions in Adapty." --- The 1000-code error, `noProductIDsFound`, indicates that none of the products you requested on the paywall are available for purchase in the App Store, even though they’re listed there. This error may sometimes come with an `InvalidProductIdentifiers` warning. If the warning appears without an error, safely ignore it. If you’re encountering the `noProductIDsFound` error, follow these steps to resolve it: ## Step 1. Check bundle ID \{#step-2-check-bundle-id\} --- no_index: true --- 1. Open [App Store Connect](https://appstoreconnect.apple.com/apps). Select your app and proceed to **General** → **App Information** section. 2. Copy the **Bundle ID** in the **General Information** sub-section. 3. Open the [**App settings** -> **iOS SDK** tab](https://app.adapty.io/settings/ios-sdk) from the Adapty top menu and paste the copied value to the **Bundle ID** field. 4. Get back to the **App information** page in App Store Connect and copy **Apple ID** from there. 5. On the [**App settings** -> **iOS SDK**](https://app.adapty.io/settings/ios-sdk) page in the Adapty dashboard, paste the ID to the **Apple app ID** field. ## Step 2. Check products \{#step-3-check-products\} 1. Go to **App Store Connect** and navigate to [**Monetization** → **Subscriptions**](https://appstoreconnect.apple.com/apps/6477523342/distribution/subscriptions) in the left-hand menu. 2. Click on the subscription group name. You’ll see your products listed under the **Subscriptions** section. 3. Ensure the product you're testing is marked **Ready to Submit**. If not, follow the instructions on the [Product in App Store](app-store-products) page. 4. Compare the product ID from the table with the one in the [**Products**](https://app.adapty.io/products) tab in the Adapty Dashboard. If the IDs don’t match, copy the product ID from the table and [create a product](create-product) with it in the Adapty Dashboard. ## Step 3. Check product availability \{#step-4-check-product-availability\} 1. Go back to **App Store Connect** and open the same **Subscriptions** section. 2. Click the subscription group name to view your products. 3. Select the product you're testing. 4. Scroll to the **Availability** section and check that all the required countries and regions are listed. ## Step 4. Check product prices \{#step-5-check-product-prices\} 1. Again, head to the **Monetization** → **Subscriptions** section in **App Store Connect**. 2. Click the subscription group name. 3. Select the product you’re testing. 4. Scroll down to **Subscription Pricing** and expand the **Current Pricing for New Subscribers** section. 5. Ensure that all required prices are listed. ## Step 5. Check app paid status, bank account, and tax forms are active 1. In **App Store Connect**](https://appstoreconnect.apple.com/) homepage, click **Business**. 2. Select your company name. 3. Scroll down and check that your **Paid Apps Agreement**, **Bank Account**, and **Tax forms** all show as **Active**. By following these steps, you should be able to resolve the `InvalidProductIdentifiers` warning and get your products live in the store --- # File: cantMakePayments --- --- title: "Fix for Code-1003 cantMakePayment error" description: "Resolve making payments error when managing subscriptions in Adapty." --- The 1003 error, `cantMakePayments`, indicates that in-app purchases can't be made on this device. If you’re encountering the `cantMakePayments` error, this is usually due to one of the reasons: - Device restrictions: The error is not related to Adapty. See the ways to fix the issue below. - Observer mode configuration: The `makePurchase` method and the observer mode can't be used at the same time. See the section below. ## Issue: Device restrictions | Issue | Solution | |---------------------------|---------------------------------------------------------| | Screen Time restrictions | Disable In-App Purchase restrictions in [Screen Time](https://support.apple.com/en-us/102470) | | Account suspended | Contact Apple Support to resolve account issues | | Regional restrictions | Use App Store account from supported region | ## Issue: Using both Observer mode and makePurchase If you are using `makePurchases` to handle purchases, you don't need to use Observer mode. [Observer mode](https://adapty.io/docs/observer-vs-full-mode) is only needed if you implement the purchase logic yourself. So, if you're using `makePurchase`, you can safely remove enabling Observer mode from the SDK activation code. --- # File: migration-to-ios-315 --- --- title: "Migrate Adapty iOS SDK to v. 3.15" description: "Migrate to Adapty iOS SDK v3.15 for better performance and new monetization features." --- If you use [Paywall Builder](adapty-paywall-builder.md) in [Observer mode](observer-vs-full-mode), starting from iOS SDK 3.15, you need to implement a new method `observerModeDidInitiateRestorePurchases(onStartRestore:onFinishRestore:)`. This method provides more control over the restore logic, allowing you to handle restore purchases in your custom flow. For complete implementation details, refer to [Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode). ```diff showLineNumbers func observerMode(didInitiatePurchase product: AdaptyPaywallProduct, onStartPurchase: @escaping () -> Void, onFinishPurchase: @escaping () -> Void) { // use the product object to handle the purchase // use the onStartPurchase and onFinishPurchase callbacks to notify AdaptyUI about the process of the purchase } + func observerModeDidInitiateRestorePurchases(onStartRestore: @escaping () -> Void, + onFinishRestore: @escaping () -> Void) { + // use the onStartRestore and onFinishRestore callbacks to notify AdaptyUI about the process of the restore + } ``` --- # File: migration-to-ios-sdk-34 --- --- title: "Migrate Adapty iOS SDK to v. 3.4" description: "Migrate to Adapty iOS SDK v3.4 for better performance and new monetization features." --- Adapty SDK 3.4.0 is a major release that introduces improvements that require migration steps on your end. ## Update Adapty SDK activation ```diff showLineNumbers // In your AppDelegate class: let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") - Adapty.activate(with: configurationBuilder) { error in + Adapty.activate(with: configurationBuilder.build()) { error in // handle the error } ``` **Update fallback paywall files** Update your fallback paywall files to ensure compatibility with the new SDK version: 1. [Download the updated fallback paywall files](fallback-paywalls#download-fallback-paywalls-as-a-file-in-the-adapty-dashboard) from the Adapty Dashboard. 2. [Replace the existing fallback paywalls in your mobile app](ios-use-fallback-paywalls) with the new files. ```diff showLineNumbers @main struct SampleApp: App { init() { let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") Task { - try await Adapty.activate(with: configurationBuilder) + try await Adapty.activate(with: configurationBuilder.build()) } } var body: some Scene { WindowGroup { ContentView() } } } ``` **Update fallback paywall files** Update your fallback paywall files to ensure compatibility with the new SDK version: 1. [Download the updated fallback paywall files](fallback-paywalls#download-fallback-paywalls-as-a-file-in-the-adapty-dashboard) from the Adapty Dashboard. 2. [Replace the existing fallback paywalls in your mobile app](ios-use-fallback-paywalls) with the new files. --- # File: migration-to-ios330 --- --- title: "Migrate Adapty iOS SDK to v. 3.3" description: "Migrate to Adapty iOS SDK v3.3 for better performance and new monetization features." --- Adapty SDK 3.3.0 is a major release that brought some improvements which however may require some migration steps from you. 1. Rename `Adapty.Configuration` to `AdaptyConfiguration`. 2. Rename the `getViewConfiguration` method to `getPaywallConfiguration`. 3. Remove the `didCancelPurchase` and `paywall` parameters from SwiftUI, and rename the `viewConfiguration` parameter to `paywallConfiguration`. 4. Update how you process promotional in-app purchases from the App Store by removing the `defermentCompletion` parameter from the `AdaptyDelegate` method. 5. Remove the `getProductsIntroductoryOfferEligibility` method. 6. Update integration configurations for Adjust, AirBridge, Amplitude, AppMetrica, Appsflyer, Branch, Facebook Ads, Firebase and Google Analytics, Mixpanel, OneSignal, Pushwoosh. 7. Update Observer mode implementation.
## Rename Adapty.Configuration to AdaptyConfiguration Update the code of Adapty iOS SDK activation in the following way: ```diff showLineNumbers // In your AppDelegate class: let configurationBuilder = - Adapty.Configuration + AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(observerMode: false) .with(customerUserId: "YOUR_USER_ID") .with(idfaCollectionDisabled: false) .with(ipAddressCollectionDisabled: false) Adapty.activate(with: configurationBuilder) { error in // handle the error } ``` ```diff showLineNumbers @main struct SampleApp: App { init() let configurationBuilder = - Adapty.Configuration + AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(observerMode: false) // optional .with(customerUserId: "YOUR_USER_ID") // optional .with(idfaCollectionDisabled: false) // optional .with(ipAddressCollectionDisabled: false) // optional Task { try await Adapty.activate(with: configurationBuilder) } } var body: some Scene { WindowGroup { ContentView() } } } ``` ## Rename getViewConfiguration method to getPaywallConfiguration Update the method name to fetch the paywall's `viewConfiguration`: ```diff showLineNumbers guard paywall.hasViewConfiguration else { // use your custom logic return } do { - let paywallConfiguration = try await AdaptyUI.getViewConfiguration( + let paywallConfiguration = try await AdaptyUI.getPaywallConfiguration( forPaywall: paywall ) // use loaded configuration } catch { // handle the error } ``` For more details about the method, check out [Fetch the view configuration of paywall designed using Paywall Builder](get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder). ## Change parameters in SwiftUI The following updates have been made to SwiftUI: 1. The `didCancelPurchase` parameter has been removed. Use `didFinishPurchase` instead. 2. The `.paywall()` method no longer accepts a paywall object. 3. The `paywallConfiguration` parameter has replaced the `viewConfiguration` parameter. Update your code like this: ```diff showLineNumbers @State var paywallPresented = false var body: some View { Text("Hello, AdaptyUI!") .paywall( isPresented: $paywallPresented, - paywall: , - viewConfiguration: , + paywallConfiguration: , didPerformAction: { action in switch action { case .close: paywallPresented = false default: // Handle other actions break } }, - didFinishPurchase: { product, profile in paywallPresented = false }, + didFinishPurchase: { product, purchaseResult in /* handle the result*/ }, didFailPurchase: { product, error in /* handle the error */ }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, didFailRendering: { error in paywallPresented = false } - didCancelPurchase: { product in /* handle the result*/} ) } ``` ## Update handling of promotional in-app purchases from App Store Update how you handle promotional in-app purchases from the App Store by removing the `defermentCompletion` parameter from the `AdaptyDelegate` method, as shown in the example below: ```swift showLineNumbers title="Swift" final class YourAdaptyDelegateImplementation: AdaptyDelegate { nonisolated func shouldAddStorePayment(for product: AdaptyDeferredProduct) -> Bool { // 1a. // Return `true` to continue the transaction in your app. // 1b. // Store the product object and return `false` to defer or cancel the transaction. false } // 2. Continue the deferred purchase later on by passing the product to `makePurchase` func continueDeferredPurchase() async { let storedProduct: AdaptyDeferredProduct = // get the product object from the 1b. do { try await Adapty.makePurchase(product: storedProduct) } catch { // handle the error } } } ``` ## Remove getProductsIntroductoryOfferEligibility method Before Adapty iOS SDK 3.3.0, the product object always included offers, regardless of whether the user was eligible. You had to manually check eligibility before using the offer. Now, the product object only includes an offer if the user is eligible. This means you no longer need to check eligibility — if an offer is present, the user is eligible. If you still want to view offers for users who are not eligible, refer to `sk1Product` and `sk2Product`. ## Update third-party integration SDK configuration Starting with Adapty iOS SDK 3.3.0, we’ve updated the public API for the `updateAttribution` method. Previously, it accepted a `[AnyHashable: Any]` dictionary, allowing you to pass attribution objects directly from various services. Now, it requires a `[String: any Sendable]`, so you’ll need to convert attribution objects before passing them. To ensure integrations work properly with Adapty iOS SDK 3.3.0 and later, update your SDK configurations for the following integrations as described in the sections below. ### Adjust Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Adjust integration](adjust#sdk-configuration). ```diff showLineNumbers class AdjustModuleImplementation { - func updateAdjustAttribution() { - Adjust.attribution { attribution in - guard let attributionDictionary = attribution?.dictionary()?.toSendableDict() else { return } - - Adjust.adid { adid in - guard let adid else { return } - - Adapty.updateAttribution(attributionDictionary, source: .adjust, networkUserId: adid) { error in - // handle the error - } - } - } - } + func updateAdjustAdid() { + Adjust.adid { adid in + guard let adid else { return } + + Adapty.setIntegrationIdentifier(key: "adjust_device_id", value: adid) + } + } + + func updateAdjustAttribution() { + Adjust.attribution { attribution in + guard let attribution = attribution?.dictionary() else { + return + } + + Adapty.updateAttribution(attribution, source: "adjust") + } + } } ``` ```diff showLineNumbers class YourAdjustDelegateImplementation { // Find your implementation of AdjustDelegate // and update adjustAttributionChanged method: func adjustAttributionChanged(_ attribution: ADJAttribution?) { - if let attribution = attribution?.dictionary()?.toSendableDict() { - Adapty.updateAttribution(attribution, source: .adjust) + if let attribution = attribution?.dictionary() { + Adapty.updateAttribution(attribution, source: "adjust") } } } ``` ### AirBridge Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for AirBridge integration](airbridge#sdk-configuration). ```diff showLineNumbers import AirBridge - let builder = AdaptyProfileParameters.Builder() - .with(airbridgeDeviceId: AirBridge.deviceUUID()) - - Adapty.updateProfile(params: builder.build()) + do { + try await Adapty.setIntegrationIdentifier( + key: "airbridge_device_id", + value: AirBridge.deviceUUID() + ) + } catch { + // handle the error + } ``` ### Amplitude Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Amplitude integration](amplitude#sdk-configuration). ```diff showLineNumbers import Amplitude - let builder = AdaptyProfileParameters.Builder() - .with(amplitudeUserId: Amplitude.instance().userId) - .with(amplitudeDeviceId: Amplitude.instance().deviceId) - - Adapty.updateProfile(params: builder.build()) + do { + try await Adapty.setIntegrationIdentifier( + key: "amplitude_user_id", + value: Amplitude.instance().userId + ) + try await Adapty.setIntegrationIdentifier( + key: "amplitude_device_id", + value: Amplitude.instance().deviceId + ) + } catch { + // handle the error + } ``` ### AppMetrica Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for AppMetrica integration](appmetrica#sdk-configuration). ```diff showLineNumbers import AppMetricaCore - if let deviceID = AppMetrica.deviceID { - let builder = AdaptyProfileParameters.Builder() - .with(appmetricaDeviceId: deviceID) - .with(appmetricaProfileId: "YOUR_ADAPTY_CUSTOMER_USER_ID") - - Adapty.updateProfile(params: builder.build()) - } + if let deviceID = AppMetrica.deviceID { + do { + try await Adapty.setIntegrationIdentifier( + key: "appmetrica_device_id", + value: deviceID + ) + try await Adapty.setIntegrationIdentifier( + key: "appmetrica_profile_id", + value: "YOUR_ADAPTY_CUSTOMER_USER_ID" + ) + } catch { + // handle the error + } + } ``` ### AppsFlyer Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for AppsFlyer integration](appsflyer#sdk-configuration). ```diff showLineNumbers class YourAppsFlyerLibDelegateImplementation { // Find your implementation of AppsFlyerLibDelegate // and update onConversionDataSuccess method: func onConversionDataSuccess(_ conversionInfo: [AnyHashable : Any]) { let uid = AppsFlyerLib.shared().getAppsFlyerUID() - Adapty.updateAttribution( - conversionInfo.toSendableDict(), - source: .appsflyer, - networkUserId: uid - ) + Adapty.setIntegrationIdentifier(key: "appsflyer_id", value: uid) + Adapty.updateAttribution(conversionInfo, source: "appsflyer") } } ``` ### Branch Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Branch integration](branch#sdk-configuration). ```diff showLineNumbers class YourBranchImplementation { func initializeBranch() { // Pass the attribution you receive from the initializing method of Branch iOS SDK to Adapty. Branch.getInstance().initSession(launchOptions: launchOptions) { (data, error) in - if let data = data?.toSendableDict() { - Adapty.updateAttribution(data, source: .branch) - } + if let data { + Adapty.updateAttribution(data, source: "branch") + } } } } ``` ### Facebook Ads Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Facebook Ads integration](facebook-ads#sdk-configuration). ```diff showLineNumbers import FacebookCore - let builder = AdaptyProfileParameters.Builder() - .with(facebookAnonymousId: AppEvents.shared.anonymousID) - - do { - try Adapty.updateProfile(params: builder.build()) - } catch { - // handle the error - } + do { + try await Adapty.setIntegrationIdentifier( + key: "facebook_anonymous_id", + value: AppEvents.shared.anonymousID + ) + } catch { + // handle the error + } ``` ### Firebase and Google Analytics Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Firebase and Google Analytics integration](firebase-and-google-analytics). ```diff showLineNumbers import FirebaseCore import FirebaseAnalytics FirebaseApp.configure() - if let appInstanceId = Analytics.appInstanceID() { - let builder = AdaptyProfileParameters.Builder() - .with(firebaseAppInstanceId: appInstanceId) - Adapty.updateProfile(params: builder.build()) { error in - // handle error - } - } + if let appInstanceId = Analytics.appInstanceID() { + do { + try await Adapty.setIntegrationIdentifier( + key: "firebase_app_instance_id", + value: appInstanceId + ) + } catch { + // handle the error + } + } ``` ### Mixpanel Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Mixpanel integration](mixpanel#sdk-configuration). ```diff showLineNumbers import Mixpanel - let builder = AdaptyProfileParameters.Builder() - .with(mixpanelUserId: Mixpanel.mainInstance().distinctId) - - do { - try await Adapty.updateProfile(params: builder.build()) - } catch { - // handle the error - } + do { + try await Adapty.setIntegrationIdentifier( + key: "mixpanel_user_id", + value: Mixpanel.mainInstance().distinctId + ) + } catch { + // handle the error + } ``` ### OneSignal Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for OneSignal integration](onesignal#sdk-configuration). ```diff showLineNumbers // PlayerID (pre-v5 OneSignal SDK) // in your OSSubscriptionObserver implementation func onOSSubscriptionChanged(_ stateChanges: OSSubscriptionStateChanges) { if let playerId = stateChanges.to.userId { - let params = AdaptyProfileParameters.Builder() - .with(oneSignalPlayerId: playerId) - .build() - - Adapty.updateProfile(params:params) { error in - // check error - } + Task { + try await Adapty.setIntegrationIdentifier( + key: "one_signal_player_id", + value: playerId + ) + } } } // SubscriptionID (v5+ OneSignal SDK) OneSignal.Notifications.requestPermission({ accepted in - let id = OneSignal.User.pushSubscription.id - - let builder = AdaptyProfileParameters.Builder() - .with(oneSignalSubscriptionId: id) - - Adapty.updateProfile(params: builder.build()) + Task { + try await Adapty.setIntegrationIdentifier( + key: "one_signal_subscription_id", + value: OneSignal.User.pushSubscription.id + ) + } }, fallbackToSettings: true) ``` ### Pushwoosh Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Pushwoosh integration](pushwoosh#sdk-configuration). ```diff showLineNumbers - let params = AdaptyProfileParameters.Builder() - .with(pushwooshHWID: Pushwoosh.sharedInstance().getHWID()) - .build() - - Adapty.updateProfile(params: params) { error in - // handle the error - } + do { + try await Adapty.setIntegrationIdentifier( + key: "pushwoosh_hwid", + value: Pushwoosh.sharedInstance().getHWID() + ) + } catch { + // handle the error + } ``` ## Update Observer mode implementation Update how you link paywalls to transactions. Previously, you used the `setVariationId` method to assign the `variationId`. Now, you can include the `variationId` directly when recording the transaction using the new `reportTransaction` method. Check out the final code example in the [Associate paywalls with purchase transactions in Observer mode](report-transactions-observer-mode). :::warning Remember to record the transaction using the `reportTransaction` method. Skipping this step means Adapty won't recognize the transaction, grant access levels, include it in analytics, or send it to integrations. This step is essential! ::: ```diff showLineNumbers - let variationId = paywall.variationId - - // There are two overloads: for StoreKit 1 and StoreKit 2 - Adapty.setVariationId(variationId, forPurchasedTransaction: transaction) { error in - if error == nil { - // successful binding - } - } + do { + // every time when calling transaction.finish() + try await Adapty.reportTransaction(transaction, withVariationId: ) + } catch { + // handle the error + } ``` --- # File: migration-to-ios-sdk-v3 --- --- title: "Migrate Adapty iOS SDK to v. 3.0" description: "Migrate to Adapty iOS SDK v3.0 for better performance and new monetization features." --- Adapty SDK v.3.0 brings support for the new exciting [Adapty Paywall Builder](adapty-paywall-builder), the new version of the no-code user-friendly tool to create paywalls. With its maximum flexibility and rich design capabilities, your paywalls will become most effective and profitable. :::info Please note that the AdaptyUI library is deprecated and is now included as part of AdaptySDK. ::: ## Reinstall Adapty SDK v3.x via Swift Package Manager 1. Delete AdaptyUI SDK package dependency from your project, you won't need it anymore. 2. Even though you have it already, you'll need to re-add the Adapty SDK dependency. For this, in Xcode, open **File** -> **Add Package Dependency...**. Please note the way to add package dependencies can differ in XCode versions. Refer to XCode documentation if necessary. 3. Enter the repository URL `https://github.com/adaptyteam/AdaptySDK-iOS.git` 4. Choose the version, and click the **Add package** button. 5. Choose the modules you need: 1. **Adapty** is the mandatory module 2. **AdaptyUI** is an optional module you need if you plan to use the [Adapty Paywall Builder](adapty-paywall-builder). 6. Xcode will add the package dependency to your project, and you can import it. For this, in the **Choose Package Products** window, click the **Add package** button once again. The package will appear in the **Packages** list. ## Reinstall Adapty SDK v3.x via CocoaPods 1. Add Adapty to your `Podfile`. Choose the modules you need: 1. **Adapty** is the mandatory module. 2. **AdaptyUI** is an optional module you need if you plan to use the [Adapty Paywall Builder](adapty-paywall-builder). 2. ```shell showLineNumbers title="Podfile" pod 'Adapty', '~> 3.2.0' pod 'AdaptyUI', '~> 3.2.0' # optional module needed only for Paywall Builder ``` 3. Run: ```sh showLineNumbers title="Shell" pod install ``` This creates a `.xcworkspace` file for your app. Use this file for all future development of your application. Activate Adapty and AdaptyUI SDK modules. Before v3.0, you did not activate AdaptyUI, remember to **add AdaptyUI activation**. Parameters are not changes, so keep them as is. ```swift showLineNumbers // In your AppDelegate class: let configurationBuilder = AdaptyConfiguration .Builder(withAPIKey: "PUBLIC_SDK_KEY") .with(observerMode: false) .with(customerUserId: "YOUR_USER_ID") .with(idfaCollectionDisabled: false) .with(ipAddressCollectionDisabled: false) Adapty.activate(with: configurationBuilder) { error in // handle the error } // Only if you are going to use AdaptyUI AdaptyUI.activate() ``` ```swift title="" showLineNumbers @main struct SampleApp: App { init() let configurationBuilder = AdaptyConfiguration .Builder(withAPIKey: "PUBLIC_SDK_KEY") .with(observerMode: false) // optional .with(customerUserId: "YOUR_USER_ID") // optional .with(idfaCollectionDisabled: false) // optional .with(ipAddressCollectionDisabled: false) // optional Adapty.activate(with: configurationBuilder) { error in // handle the error } // Only if you are going to use AdaptyUI AdaptyUI.activate() } var body: some Scene { WindowGroup { ContentView() } } } ``` --- # File: ios-legacy-install --- --- title: "Legacy installation guide" description: "Get started with Adapty on iOS to streamline subscription setup and management." --- Please consult the compatibility table below to choose the correct pair of Adapty SDK and AdaptyUI SDK. | Adapty SDK version | AdaptyUI SDK version | | :----------------------------------- | :------------------- | | 2.7.x, 2.8.x | 2.0.x | | 2.9.x - 2.10.0 | 2.1.2 | | 2.10.1 | 2.1.3 | | 2.10.3 and all later 2.10.x versions | 2.1.5 | | 2.11.1 | 2.11.1 | | 2.11.2 | 2.11.2 | | 2.11.3 | 2.11.3 | You can install AdaptySDK and AdaptyUI SDK via CocoaPods, or Swift Package Manager. :::danger Go through release checklist before releasing your app Before releasing your application, make sure to carefully review the [Release Checklist](release-checklist) thoroughly. This checklist ensures that you've completed all necessary steps and provides criteria for evaluating the success of your integration. ::: ## Install SDKs via Swift Package Manager 1. In Xcode go to **File** -> **Add Package Dependency...**. Please note the way to add package dependencies can differ in XCode versions. Refer to XCode documentation if necessary. 2. Enter the repository URL `https://github.com/adaptyteam/AdaptySDK-iOS.git` 3. Choose the version, and click the **Add package** button. Xcode will add the package dependency to your project, and you can import it. 4. In the **Choose Package Products** window, click the **Add package** button once again. The package will appear in the **Packages** list. 5. Repeat steps 2-3 for AdaptyUI SDK URL: `https://github.com/adaptyteam/AdaptyUI-iOS.git`. ## Install SDKs via CocoaPods :::info CocoaPods is now in maintenance mode, with development officially stopped. We recommend switching to [Swift Package Manager](sdk-installation-ios#install-adapty-sdk-via-swift-package-manager). ::: 1. Add Adapty to your `Podfile`: ```shell showLineNumbers title="Podfile" pod 'Adapty', '~> 2.11.3' pod 'AdaptyUI', '~> 2.11.3' ``` 2. Run: ```sh showLineNumbers title="Shell" pod install ``` This creates a `.xcworkspace` file for your app. Use this file for all future development of your application. ## Configure Adapty SDK You only need to configure the Adapty SDK once, typically early in your application lifecycle: ```swift showLineNumbers // In your AppDelegate class: let configurationBuilder = AdaptyConfiguration .Builder(withAPIKey: "PUBLIC_SDK_KEY") .with(observerMode: false) // optional .with(customerUserId: "YOUR_USER_ID") // optional .with(idfaCollectionDisabled: false) // optional .with(ipAddressCollectionDisabled: false) // optional Adapty.activate(with: configurationBuilder.build()) { error in // handle the error } ``` ```swift showLineNumbers @main struct SampleApp: App { init() let configurationBuilder = AdaptyConfiguration .Builder(withAPIKey: "PUBLIC_SDK_KEY") .with(observerMode: false) // optional .with(customerUserId: "YOUR_USER_ID") // optional .with(idfaCollectionDisabled: false) // optional .with(ipAddressCollectionDisabled: false) // optional .with(LogLevel: verbose) // optional Adapty.activate(with: configurationBuilder.build()) { error in // handle the error } } var body: some Scene { WindowGroup { ContentView() } } } ``` Parameters: | Parameter | Presence | Description | | --------------------------- | -------- | ------------------------------------------------------------ | | apiKey | required | The key you can find in the **Public SDK key** field of your app settings in Adapty: [**App settings**-> **General** tab -> **API keys** subsection](https://app.adapty.io/settings/general) | | observerMode | optional |

A boolean value controlling [Observer mode](observer-vs-full-mode). Turn it on if you handle purchases and subscription status yourself and use Adapty for sending subscription events and analytics.

The default value is `false`.

🚧 When running in Observer mode, Adapty SDK won't close any transactions, so make sure you're handling it.

| | customerUserId | optional | An identifier of the user in your system. We send it in subscription and analytical events, to attribute events to the right profile. You can also find customers by `customerUserId` in the [**Profiles and Segments**](https://app.adapty.io/profiles/users) menu. | | idfaCollectionDisabled | optional |

Set to `true` to disable IDFA collection and sharing.

the user IP address sharing.

The default value is `false`.

For more details on IDFA collection, refer to the [Analytics integration](analytics-integration#disable-collection-of-advertising-identifiers) section.

| | ipAddressCollectionDisabled | optional |

Set to `true` to disable user IP address collection and sharing.

The default value is `false`.

| | logLevel | optional | Adapty logs errors and other crucial information to provide insight into your app's functionality. There are the following available levels:
  • error: Only errors will be logged.
  • warn: Errors and messages from the SDK that do not cause critical errors, but are worth paying attention to will be logged.
  • info: Errors, warnings, and serious information messages, such as those that log the lifecycle of various modules will be logged.
  • verbose: Any additional information that may be useful during debugging, such as function calls, API queries, etc. will be logged.
| :::note - Note, that StoreKit 2 is available since iOS 15.0. Adapty will implement the legacy logic for older versions. - Make sure you use the **Public SDK key** for Adapty initialization, the **Secret key** should be used for [server-side API](getting-started-with-server-side-api) only. - **SDK keys** are unique for every app, so if you have multiple apps make sure you choose the right one. ::: Please keep in mind that for paywalls and products to be displayed in your mobile application, and for analytics to work, you need to [display the paywalls](ios-present-paywalls) and, if you're using paywalls not created with the Paywall Builder, [handle the purchase process](making-purchases) within your app. --- # File: ios-get-legacy-pb-paywalls --- --- title: "Fetch legacy Paywall Builder paywalls in iOS SDK" description: "Retrieve legacy PB paywalls in your iOS app with Adapty SDK." --- After [you designed the visual part for your paywall](adapty-paywall-builder-legacy) with Paywall Builder in the Adapty Dashboard, you can display it in your iOS app. The first step in this process is to get the paywall associated with the placement and its view configuration as described below. :::warning This guide is for **legacy Paywall Builder paywalls** only which require SDK v2.x or earlier. The process for fetching paywalls differs for paywalls designed with different versions of Paywall Builder and remote config paywalls. - For fetching **New Paywall Builder paywalls**, check out [Fetch new Paywall Builder paywalls and their configuration](get-pb-paywalls). - For fetching **Remote config paywalls**, see [Fetch paywalls and products for remote config paywalls](fetch-paywalls-and-products). :::
Before you start displaying paywalls in your iOS app (click to expand) 1. [Create your products](create-product) in the Adapty Dashboard. 2. [Create a paywall and incorporate the products into it](create-paywall) in the Adapty Dashboard. 3. [Create placements and incorporate your paywall into it](create-placement) in the Adapty Dashboard. 4. [Install Adapty SDK and AdaptyUI DSK](sdk-installation-ios) in your iOS app.
## Fetch paywall designed with Paywall Builder If you've [designed a paywall using the Paywall Builder](adapty-paywall-builder-legacy), you don't need to worry about rendering it in your iOS app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. Nevertheless, you need to get its ID via the placement, its view configuration, and then present it in your iOS app. To ensure optimal performance, it's crucial to retrieve the paywall and its [view configuration](#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) as early as possible, allowing sufficient time for images to download before presenting them to the user. To get a paywall, use the `getPaywall` method: ```swift showLineNumbers Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en") { result in switch result { case let .success(paywall): // the requested paywall case let .failure(error): // handle the error } } ``` | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the [paywall localization](add-paywall-locale-in-adapty-paywall-builder). This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.

| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

Adapty SDK stores paywalls locally in two layers: regularly updated cache described above and [fallback paywalls](fallback-paywalls). We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.

| **Don't hardcode product IDs.** The only ID you should hardcode is the placement ID. Paywalls are configured remotely, so the number of products and available offers can change at any time. Your app must handle these changes dynamically—if a paywall returns two products today and three tomorrow, display all of them without code changes. Response parameters: | Parameter | Description | | :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](https://swift.adapty.io/documentation/adapty/adaptypaywall) object with a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch the view configuration of paywall designed using Paywall Builder After fetching the paywall, check if it includes a `viewConfiguration`, which indicates that it was created using Paywall Builder. This will guide you on how to display the paywall. If the `viewConfiguration` is present, treat it as a Paywall Builder paywall; if not, [handle it as a remote config paywall](present-remote-config-paywalls). Use the `getViewConfiguration` method to load the view configuration. ```swift showLineNumbers do { guard paywall.hasViewConfiguration else { // use your custom logic return } let paywallConfiguration = try await AdaptyUI.getPaywallConfiguration(forPaywall: paywall) // use loaded configuration } catch { // handle the error } ``` --- # File: ios-present-paywalls-legacy --- --- title: "Present legacy Paywall Builder paywalls in iOS SDK" description: "Discover how to present paywalls in iOS using Adapty’s legacy methods." --- If you've customized a paywall using the Paywall Builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. :::warning This guide is for **legacy Paywall Builder paywalls** only which require SDK v2.x or earlier. The process for presenting paywalls differs for paywalls designed with different versions of Paywall Builde, remote config paywalls, and [Observer mode](observer-vs-full-mode). - For presenting **New Paywall Builder paywalls**, check out [iOS - Present new Paywall Builder paywalls](ios-present-paywalls). - For presenting **Remote config paywalls**, see [Render paywall designed by remote config](present-remote-config-paywalls). - For presenting **Observer mode paywalls**, see [iOS - Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode) ::: ## Present paywalls in Swift In order to display the visual paywall on the device screen, you must first configure it. To do this, use the method `.paywallController(for:products:viewConfiguration:delegate:)`: ```swift showLineNumbers title="Swift" let visualPaywall = AdaptyUI.paywallController( for: , products: , viewConfiguration: , delegate: ) ``` Request parameters: | Parameter | Presence | Description | | :-------------------- | :------- | :----------------------------------------------------------- | | **Paywall** | required | An `AdaptyPaywall` object to obtain a controller for the desired paywall. | | **Products** | optional | Provide an array of `AdaptyPaywallProducts` to optimize the display timing of products on the screen. If `nil` is passed, AdaptyUI will automatically fetch the necessary products. | | **ViewConfiguration** | required | An `AdaptyUI.LocalizedViewConfiguration` object containing visual details of the paywall. Use the `AdaptyUI.getViewConfiguration(paywall:locale:)` method. Refer to [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls) topic for more details. | | **Delegate** | required | An `AdaptyPaywallControllerDelegate` to listen to paywall events. Refer to [Handling paywall events](ios-handling-events) topic for more details. | | **TagResolver** | optional | Define a dictionary of custom tags and their resolved values. Custom tags serve as placeholders in the paywall content, dynamically replaced with specific strings for personalized content within the paywall. Refer to [Custom tags in Paywall Builder](custom-tags-in-paywall-builder) topic for more details. | Returns: | Object | Description | | :---------------------- | :--------------------------------------------------- | | AdaptyPaywallController | An object, representing the requested paywall screen | After the object has been successfully created, you can display it on the screen of the device: ```swift showLineNumbers title="Swift" present(visualPaywall, animated: true) ``` ## Present paywalls in SwiftUI In order to display the visual paywall on the device screen, use the `.paywall` modifier in SwiftUI: ```swift showLineNumbers title="SwiftUI" @State var paywallPresented = false var body: some View { Text("Hello, AdaptyUI!") .paywall( isPresented: $paywallPresented, paywall: , configuration: , didPerformAction: { action in switch action { case .close: paywallPresented = false default: // Handle other actions break } }, didFinishPurchase: { product, profile in paywallPresented = false }, didFailPurchase: { product, error in /* handle the error */ }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, didFailRendering: { error in paywallPresented = false } ) } ``` Request parameters: | Parameter | Presence | Description | | :---------------- | :------- | :----------------------------------------------------------- | | **Paywall** | required | An `AdaptyPaywall` object to obtain a controller for the desired paywall. | | **Product** | optional | Provide an array of `AdaptyPaywallProducts` to optimize the display timing of products on the screen. If `nil` is passed, AdaptyUI will automatically fetch the necessary products. | | **Configuration** | required | An `AdaptyUI.LocalizedViewConfiguration` object containing visual details of the paywall. Use the `AdaptyUI.getViewConfiguration(paywall:locale:)` method. Refer to [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls) topic for more details. | | **TagResolver** | optional | Define a dictionary of custom tags and their resolved values. Custom tags serve as placeholders in the paywall content, dynamically replaced with specific strings for personalized content within the paywall. Refer to [Custom tags in Paywall Builder](custom-tags-in-paywall-builder) topic for more details. | Closure parameters: | Closure parameter | Description | | :-------------------- | :-------------------------------------------------------------------------------- | | **didFinishPurchase** | If Adapty.makePurchase() succeeds, this callback will be invoked. | | **didFailPurchase** | If Adapty.makePurchase() fails, this callback will be invoked. | | **didFinishRestore** | If Adapty.restorePurchases() succeeds, this callback will be invoked. | | **didFailRestore** | If Adapty.restorePurchases() fails, this callback will be invoked. | | **didFailRendering** | If an error occurs during the interface rendering, this callback will be invoked. | Refer to the [iOS - Handling events](ios-handling-events) topic for other closure parameters. **Next step:** - [Handle paywall events](ios-handling-events-legacy) --- # File: ios-handling-events-legacy --- --- title: "Handle paywall events in legacy iOS SDK" description: "Handle events in iOS (Legacy) apps with Adapty’s event tracking system." --- Paywalls configured with the [Paywall Builder](adapty-paywall-builder-legacy) don't need extra code to make and restore purchases. However, they generate some events that your app can respond to. Those events include button presses (close buttons, URLs, product selections, and so on) as well as notifications on purchase-related actions taken on the paywall. Learn how to respond to these events below. :::warning This guide is for **legacy Paywall Builder paywalls** only which require Adapty SDK up to v2.x. For presenting paywalls in Adapty SDK v3.0 or later designed with new Paywall Builder, see [iOS - Handle paywall events designed with new Paywall Builder](ios-handling-events). ::: ## Handling events in Swift To control or monitor processes occurring on the paywall screen within your mobile app, implement the `AdaptyPaywallControllerDelegate` methods. ### User-generated events #### Actions If a user performs some action (`close`, `openURL(url:)` or `custom(id:)`), the method below will be invoked. Note that this is just an example and you can implement the response to actions differently: ```swift showLineNumbers title="Swift" func paywallController(_ controller: AdaptyPaywallController, didPerform action: AdaptyUI.Action) { switch action { case .close: controller.dismiss(animated: true) case let .openURL(url): // handle URL opens (incl. terms and privacy links) UIApplication.shared.open(url, options: [:]) case let .custom(id): if id == "login" { // implement login flow } break } } ``` For example, if a user taps the close button, the action `close` will occur and you are supposed to dismiss the paywall. Note that at the very least you need to implement the reactions to both `close` and `openURL`. > 💡 Login Action > > If you have configured Login Action in the dashboard, you should also implement reaction for custom action with id `"login"`. #### Product selection If a user selects a product for purchase, this method will be invoked: ```swift showLineNumbers title="Swift" func paywallController(_ controller: AdaptyPaywallController, didSelectProduct product: AdaptyPaywallProduct) { } ``` #### Started purchase If a user initiates the purchase process, this method will be invoked: ```swift showLineNumbers title="Swift" func paywallController(_ controller: AdaptyPaywallController, didStartPurchase product: AdaptyPaywallProduct) { } ``` It will not be invoked in Observer mode. Refer to the [iOS - Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode) topic for details. #### Canceled purchase If a user initiates the purchase process but manually interrupts it, the method below will be invoked. This event occurs when the `Adapty.makePurchase()` function completes with a `.paymentCancelled` error: ```swift showLineNumbers title="Swift" func paywallController(_ controller: AdaptyPaywallController, didCancelPurchase product: AdaptyPaywallProduct) { } ``` It will not be invoked in Observer mode. Refer to the [iOS - Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode) topic for details. #### Successful purchase If `Adapty.makePurchase()` succeeds, this method will be invoked: ```swift showLineNumbers title="Swift" func paywallController(_ controller: AdaptyPaywallController, didFinishPurchase product: AdaptyPaywallProduct, purchasedInfo: AdaptyPurchasedInfo) { controller.dismiss(animated: true) } ``` We recommend dismissing the paywall screen in that case. It will not be invoked in Observer mode. Refer to the [iOS - Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode) topic for details. #### Failed purchase If `Adapty.makePurchase()` fails, this method will be invoked: ```swift showLineNumbers title="Swift" func paywallController(_ controller: AdaptyPaywallController, didFailPurchase product: AdaptyPaywallProduct, error: AdaptyError) { } ``` It will not be invoked in Observer mode. Refer to the [iOS - Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode) topic for details. #### Successful restore If `Adapty.restorePurchases()` succeeds, this method will be invoked: ```swift showLineNumbers title="Swift" func paywallController(_ controller: AdaptyPaywallController, didFinishRestoreWith profile: AdaptyProfile) { } ``` We recommend dismissing the screen if a the has the required `accessLevel`. Refer to the [Subscription status](subscription-status) topic to learn how to check it. #### Failed restore If `Adapty.restorePurchases()` fails, this method will be invoked: ```swift showLineNumbers title="Swift" public func paywallController(_ controller: AdaptyPaywallController, didFailRestoreWith error: AdaptyError) { } ``` ### Data fetching and rendering #### Product loading errors If you don't pass the product array during the initialization, AdaptyUI will retrieve the necessary objects from the server by itself. If this operation fails, AdaptyUI will report the error by calling this method: ```swift showLineNumbers title="Swift" public func paywallController(_ controller: AdaptyPaywallController, didFailLoadingProductsWith error: AdaptyError) -> Bool { return true } ``` If you return `true`, AdaptyUI will repeat the request after 2 seconds. #### Rendering errors If an error occurs during the interface rendering, it will be reported by this method: ```swift showLineNumbers title="Swift" public func paywallController(_ controller: AdaptyPaywallController, didFailRenderingWith error: AdaptyError) { } ``` In a normal situation, such errors should not occur, so if you come across one, please let us know. ## Handling events in SwiftUI To control or monitor processes occurring on the paywall screen within your mobile app, use the `.paywall` modifier in SwiftUI: ```swift showLineNumbers title="Swift" @State var paywallPresented = false var body: some View { Text("Hello, AdaptyUI!") .paywall( isPresented: $paywallPresented, paywall: paywall, configuration: viewConfig, didPerformAction: { action in switch action { case .close: paywallPresented = false case .openURL(url): // handle opening the URL (incl. for terms and privacy) case default: // handle other actions break } }, didSelectProduct: { /* Handle the event */ }, didStartPurchase: { /* Handle the event */ }, didFinishPurchase: { product, info in /* Handle the event */ }, didFailPurchase: { product, error in /* Handle the event */ }, didCancelPurchase: { /* Handle the event */ }, didStartRestore: { /* Handle the event */ }, didFinishRestore: { /* Handle the event */ }, didFailRestore: { /* Handle the event */ }, didFailRendering: { error in paywallPresented = false }, didFailLoadingProducts: { error in return false } ) } ``` You can register only the closure parameters you need, and omit those you do not need. In this case, unused closure parameters will not be created. | Closure parameter | Description | | :------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **didSelectProduct** | If a user selects a product for purchase, this parameter will be invoked. | | **didStartPurchase** | If a user initiates the purchase process, this parameter will be invoked. | | **didFinishPurchase** | If Adapty.makePurchase() succeeds, this parameter will be invoked. | | **didFailPurchase** | If Adapty.makePurchase() fails, this parameter will be invoked. | | **didCancelPurchase** | If a user initiates the purchase process but manually interrupts it, this parameter will be invoked. | | **didStartRestore** | If a user initiates the purchase restoration, this parameter will be invoked. | | **didFinishRestore** | If `Adapty.restorePurchases()` succeeds, this parameter will be invoked. | | **didFailRestore** | If `Adapty.restorePurchases()` fails, this parameter will be invoked. | | **didFailRendering** | If an error occurs during the interface rendering, this parameter will be invoked. | | **didFailLoadingProducts** | If you don't pass the product array during the initialization, AdaptyUI will retrieve the necessary objects from the server by itself. If this operation fails, AdaptyUI will invoke this parameter. | Note that at the very least you need to implement the reactions to both `close` and `openURL`. --- # End of Documentation _Generated on: 2026-03-05T16:27:48.138Z_ _Successfully processed: 43/43 files_ # KMP - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Generated on: 2026-03-05T16:27:48.140Z Total files: 41 --- # File: kmp-sdk-overview --- --- title: "Kotlin Multiplatform SDK overview" description: "Learn about Adapty Kotlin Multiplatform SDK and its key features." --- [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-KMP.svg?style=flat&logo=kotlin)](https://github.com/adaptyteam/AdaptySDK-KMP/releases) Welcome! We're here to make in-app purchases a breeze 🚀 We've built the Adapty Kotlin Multiplatform SDK to take the headache out of in-app purchases so you can focus on what you do best – building amazing apps. Here's what we handle for you: - Handle purchases, receipt validation, and subscription management out of the box - Create and test paywalls without app updates - Get detailed purchase analytics with zero setup - cohorts, LTV, churn, and funnel analysis included - Keep the user subscription status always up to date across app sessions and devices - Integrate your app with marketing attribution and analytics services using just one line of code :::note Before diving into the code, you'll need to integrate Adapty with Google Play Console and set up products in the dashboard. Check out our [quickstart guide](quickstart.md) to get everything configured first. ::: ## Get started :::tip Our docs are optimized for use with LLMs. Check out [this article](adapty-cursor-kmp.md) to learn how to get the best results when integrating the Adapty SDK using AI with our docs. ::: Here's what we'll cover in the integration guide: 1. [Install & configure SDK](sdk-installation-kotlin-multiplatform.md): Add the SDK as a dependency to your project and activate it in the code. 2. [Enable purchases through paywalls](kmp-quickstart-paywalls.md): Set up the purchase flow so users can buy products. 3. [Check the subscription status](kmp-check-subscription-status.md): Automatically check the user's subscription state and control their access to paid content. 4. [Identify users (optional)](kmp-quickstart-identify.md): Associate users with their Adapty profiles to ensure their data is stored consistently across devices. ### See it in action Want to see how it all comes together? We've got you covered: - **Sample app**: Check out our [complete example](https://github.com/adaptyteam/AdaptySDK-KMP/tree/main/example) that demonstrates the full setup - **Video tutorial**: Follow along with our step-by-step implementation video below ## Main concepts Before diving into the code, let's get familiar with the key concepts that make Adapty work. The beauty of Adapty's approach is that only placements are hardcoded in your app. Everything else – products, paywall designs, pricing, and offers – can be managed flexibly from the Adapty dashboard without app updates: 1. [**Product**](product.md) - Anything available for purchase in your app – subscription, consumable product, or lifetime access. 2. [**Paywall**](paywalls.md) - The only way to retrieve products from Adapty and use it to its full power. We've designed it this way to make it easier to track how different product combinations affect your monetization metrics. A paywall in Adapty serves as both a specific set of your products and the visual configuration that accompanies them. 3. [**Placement**](placements.md) - A strategic point in your user journey where you want to show a paywall. Think of placements as the "where" and "when" of your monetization strategy. Common placements include: - `main` - Your primary paywall location - `onboarding` - Shown during the user onboarding flow - `settings` - Accessible from your app's settings Start with the basics like `main` or `onboarding` for your first integration, then [think about where else in your app users might be ready to purchase](choose-meaningful-placements.md). 4. [**Profile**](profiles-crm.md) - When users purchase a product, their profile is assigned an **access level** which you use to define access to paid features. --- # File: sdk-installation-kotlin-multiplatform --- --- title: "Install & configure the Adapty Kotlin Multiplatform SDK" description: "Install and configure Adapty SDK for Kotlin Multiplatform apps." --- Adapty SDK includes two key modules for seamless integration into your mobile app: - **Core Adapty**: This essential SDK is required for Adapty to function properly in your app. - **AdaptyUI**: This module is needed if you use the [Adapty Paywall Builder](adapty-paywall-builder), a user-friendly, no-code tool for easily creating cross-platform paywalls. :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample app](https://github.com/adaptyteam/AdaptySDK-KMP/tree/main/example), which demonstrates the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: For a complete implementation walkthrough, you can also see the video: ## Requirements Adapty Kotlin Multiplatform SDK is compatible with Xcode 16.2 and later. :::info Adapty is compatible with Google Play Billing Library up to 8.x. By default, Adapty works with Google Play Billing Library v.7.0.0 but, if you want to force a later version, you can manually [add the dependency](https://developer.android.com/google/play/billing/integrate#dependency). ::: ## Install Adapty SDK via Gradle Adapty SDK installation with Gradle is required for both Android and iOS apps. Choose your dependency setup method: - Standard Gradle: Add dependencies to your **module-level** `build.gradle` - If your project uses `.gradle.kts` files, add dependencies to your **module-level** `build.gradle.kts` - If you use version catalogs, add dependencies to your `libs.versions.toml` file and then, reference it in `build.gradle.kts` ```kotlin showLineNumbers kotlin { sourceSets { commonMain { dependencies { implementation libs.adapty.kmp } } } } ``` ```kotlin showLineNumbers kotlin { sourceSets { val commonMain by getting { dependencies { implementation(libs.adapty.kmp) } } } } ``` ```toml showLineNumbers // libs.versions.toml [versions] .. adapty-kmp = "" [libraries] .. adapty-kmp = { module = "io.adapty:adapty-kmp", version.ref = "adapty-kmp" } // build.gradle.kts kotlin { sourceSets { val commonMain by getting { dependencies { implementation(libs.adapty.kmp) } } } } ``` :::note If you get a Maven-related error, make sure that you have `mavenCentral()` in your Gradle scripts.
The instruction on how to add it If your project doesn't have `dependencyResolutionManagement` in your `settings.gradle`, add the following to your top-level `build.gradle` at the end of repositories: ```groovy showLineNumbers title="top-level build.gradle" allprojects { repositories { ... mavenCentral() } } ``` Otherwise, add the following to your `settings.gradle` in `repositories` of `dependencyResolutionManagement` section: ```groovy showLineNumbers title="settings.gradle" dependencyResolutionManagement { ... repositories { ... google() mavenCentral() } } ```
::: ## Activate Adapty SDK ### Basic setup Add the initialization as early as possible—typically in your shared Kotlin code for both platforms. :::note The Adapty SDK only needs to be activated once in your app. ::: ```kotlin title="Kotlin" showLineNumbers val config = AdaptyConfig .Builder("PUBLIC_SDK_KEY") .build() Adapty.activate(configuration = config) .onSuccess { Log.d("Adapty", "SDK initialised") } .onError { error -> Log.e("Adapty", "Adapty init error: ${error.message}") } ``` To get your **Public SDK Key**: 1. Go to Adapty Dashboard and navigate to [App settings → General](https://app.adapty.io/settings/general). 2. From the **Api keys** section, copy the **Public SDK Key** (NOT the Secret Key). 3. Replace `"YOUR_PUBLIC_SDK_KEY"` in the code. :::info - Make sure you use the Public SDK key for Adapty initialization, the Secret key should be used for [server-side API](getting-started-with-server-side-api) only. - SDK keys are unique for every app, so if you have multiple apps make sure you choose the right one. ::: Now set up paywalls in your app: - If you use [Adapty Paywall Builder](adapty-paywall-builder), first [activate the AdaptyUI module](#activate-adaptyui-module-of-adapty-sdk) below, then follow the [Paywall Builder quickstart](kmp-quickstart-paywalls). - If you build your own paywall UI, see the [quickstart for custom paywalls](kmp-quickstart-manual). ## Activate AdaptyUI module of Adapty SDK If you plan to activate the **AdaptyUI** module to use the [Adapty Paywall Builder](kmp-present-paywalls.md), make sure to set `.withActivateUI(true)` in your configuration. :::info important In your code, you must activate the core Adapty module before activating AdaptyUI. ::: ```kotlin title="Kotlin" showLineNumbers val config = AdaptyConfig .Builder("PUBLIC_SDK_KEY") .withActivateUI(true) // true for activating the AdaptyUI module .build() Adapty.activate(configuration = config) .onSuccess { Log.d("Adapty", "SDK initialised") } .onError { error -> Log.e("Adapty", "Adapty init error: ${error.message}") } ``` ## Configure Proguard (Android) Before launching your app in the production, you might need to add `-keep class com.adapty.** { *; }` to your Proguard configuration. ## Optional setup ### Logging #### Set up the logging system Adapty logs errors and other important information to help you understand what is going on. There are the following levels available: | Level | Description | | :----------------------- | :------------------------------------------------------------------------------------------------------------------------ | | `AdaptyLogLevel.NONE` | Nothing will be logged. Default value | | `AdaptyLogLevel.ERROR` | Only errors will be logged | | `AdaptyLogLevel.WARN` | Errors and messages from the SDK that do not cause critical errors, but are worth paying attention to will be logged. | | `AdaptyLogLevel.INFO` | Errors, warnings, and various information messages will be logged. | | `AdaptyLogLevel.VERBOSE` | Any additional information that may be useful during debugging, such as function calls, API queries, etc. will be logged. | You can set the log level in your app before configuring Adapty: ```kotlin title="Kotlin" showLineNumbers val config = AdaptyConfig .Builder("PUBLIC_SDK_KEY") .withLogLevel(AdaptyLogLevel.VERBOSE) // recommended for development .build() ``` ### Data policies #### Disable IP address collection and sharing When activating the Adapty module, set `ipAddressCollectionDisabled` to `true` to disable user IP address collection and sharing. The default value is `false`. Use this parameter to enhance user privacy, comply with regional data protection regulations (like GDPR or CCPA), or reduce unnecessary data collection when IP-based features aren't required for your app. ```kotlin title="Kotlin" showLineNumbers val config = AdaptyConfig .Builder("PUBLIC_SDK_KEY") .withIpAddressCollectionDisabled(true) .build() ``` #### Disable advertising ID collection and sharing When activating the Adapty module, set `appleIdfaCollectionDisabled` (iOS) or `googleAdvertisingIdCollectionDisabled` (Android) to true to disable the collection of advertising identifiers. The default value is false. Use this parameter to comply with App Store/Play Store policies, avoid triggering the App Tracking Transparency prompt, or if your app does not require advertising attribution or analytics based on advertising IDs. ```kotlin title="Kotlin" showLineNumbers val config = AdaptyConfig .Builder("PUBLIC_SDK_KEY") .withGoogleAdvertisingIdCollectionDisabled(true) // Android only .withAppleIdfaCollectionDisabled(true) // iOS only .build() ``` #### Set up media cache configuration for AdaptyUI By default, AdaptyUI caches media (such as images and videos) to improve performance and reduce network usage. You can customize the cache settings by providing a custom configuration. Use `mediaCache` to override the default cache settings: ```kotlin val config = AdaptyConfig .Builder("PUBLIC_SDK_KEY") .withMediaCacheConfiguration( AdaptyConfig.MediaCacheConfiguration( memoryStorageTotalCostLimit = 200 * 1024 * 1024, // 200 MB memoryStorageCountLimit = Int.MAX_VALUE, diskStorageSizeLimit = 200 * 1024 * 1024 // 200 MB ) ) .build() ``` ### Enable local access levels (Android) By default, [local access levels](local-access-levels.md) are disabled for Android. To enable them, set `withLocalAccessLevelAllowed` to `true`: ```kotlin title="Kotlin" showLineNumbers val config = AdaptyConfig .Builder("PUBLIC_SDK_KEY") .withGoogleLocalAccessLevelAllowed(true) .build() ``` ### Clear data on backup restore When `withAppleClearDataOnBackup` is set to `true`, the SDK detects when the app is restored from an iCloud backup and deletes all locally stored SDK data, including cached profile information, product details, and paywalls. The SDK then initializes with a clean state. Default value is `false`. :::note Only local SDK cache is deleted. Transaction history with Apple and user data on Adapty servers remain unchanged. ::: ```swift showLineNumbers val config = AdaptyConfig .Builder("PUBLIC_SDK_KEY") .withAppleClearDataOnBackup(true) .build() ``` ## Troubleshooting #### Android backup rules (Auto Backup configuration) Some SDKs (including Adapty) ship their own Android Auto Backup configuration. If you use multiple SDKs that define backup rules, the Android manifest merger can fail with an error mentioning `android:fullBackupContent`, `android:dataExtractionRules`, or `android:allowBackup`. Typical error symptoms: `Manifest merger failed: Attribute application@dataExtractionRules value=(@xml/your_data_extraction_rules) is also present at [com.other.sdk:library:1.0.0] value=(@xml/other_sdk_data_extraction_rules)` :::note These changes should be made in your Android platform directory (typically located in your project's `android/` folder). ::: To resolve this, you need to: - Tell the manifest merger to use your app's values for backup-related attributes. - Create backup rule files that merge Adapty's rules with rules from other SDKs. #### 1. Add the `tools` namespace to your manifest In your `AndroidManifest.xml` file, ensure the root `` tag includes tools: ```xml ... ``` #### 2. Override backup attributes in `` In the same `AndroidManifest.xml` file, update the `` tag so that your app provides the final values and tells the manifest merger to replace library values: ```xml ... ``` If any SDK also sets `android:allowBackup`, include it in `tools:replace` as well: ```xml tools:replace="android:allowBackup,android:fullBackupContent,android:dataExtractionRules" ``` #### 3. Create merged backup rules files Create XML files in your Android project's `res/xml/` directory that combine Adapty's rules with rules from other SDKs. Android uses different backup rule formats depending on the OS version, so creating both files ensures compatibility across all Android versions your app supports. :::note The examples below show AppsFlyer as a sample third-party SDK. Replace or add rules for any other SDKs you're using in your app. ::: **For Android 12 and higher** (uses the new data extraction rules format): ```xml title="sample_data_extraction_rules.xml" ``` **For Android 11 and lower** (uses the legacy full backup content format): ```xml title="sample_backup_rules.xml" :::important In a Kotlin Multiplatform project, apply these changes in the Android application module (the one that produces the APK/AAB), for example, `androidApp` or `app`: - Manifest: `androidApp/src/main/AndroidManifest.xml` - Backup rules XML: `androidApp/src/main/res/xml/` ::: #### Purchases fail after returning from another app in Android If the Activity that starts the purchase flow uses a non-default `launchMode`, Android may recreate or reuse it incorrectly when the user returns from Google Play, a banking app, or a browser. This can cause the purchase result to be lost or treated as canceled. To ensure purchases work correctly, use only `standard` or `singleTop` launch modes for the Activity that starts the purchase flow, and avoid any other modes. In your `AndroidManifest.xml`, ensure the Activity that starts the purchase flow is set to `standard` or `singleTop`: ```xml ``` --- # File: kmp-quickstart-paywalls --- --- title: "Enable purchases by using paywalls in Kotlin Multiplatform SDK" description: "Quickstart guide to setting up Adapty for in-app subscription management." --- To enable in-app purchases, you need to understand three key concepts: - [**Products**](product.md) – anything users can buy (subscriptions, consumables, lifetime access) - [**Paywalls**](paywalls.md) are configurations that define which products to offer. In Adapty, paywalls are the only way to retrieve products, but this design lets you modify offerings, pricing, and product combinations without touching your app code. - [**Placements**](placements.md) – where and when you show paywalls in your app (like `main`, `onboarding`, `settings`). You set up paywalls for placements in the dashboard, then request them by placement ID in your code. This makes it easy to run A/B tests and show different paywalls to different users. Adapty offers you three ways to enable purchases in your app. Select one of them depending on your app requirements: | Implementation | Complexity | When to use | |---------------------------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Adapty Paywall Builder | ✅ Easy | You [create a complete, purchase-ready paywall in the no-code builder](quickstart-paywalls). Adapty automatically renders it and handles all the complex purchase flow, receipt validation, and subscription management behind the scenes. | | Manually created paywalls | 🟡 Medium | You implement your paywall UI in your app code, but still get the paywall object from Adapty to maintain flexibility in product offerings. See the [guide](kmp-quickstart-manual). | | Observer mode | 🔴 Hard | You already have your own purchase handling infrastructure and want to keep using it. Note that the observer mode has its limitations in Adapty. See the [article](observer-vs-full-mode). | :::important **The steps below show how to implement a paywall created in the Adapty paywall builder.** If you don't want to use the paywall builder, see the [guide for handling purchases in manually created paywalls](kmp-making-purchases.md). ::: To display a paywall created in the Adapty paywall builder, in your app code, you only need to: 1. **Get the paywall**: Get the paywall from Adapty. 2. **Display the paywall and Adapty will handle purchases for you**: Show the paywall container you've got in your app. 3. **Handle button actions**: Associate user interactions with the paywall with your app's response to them. For example, open links or close the paywall when users click buttons. ## Before you start Before you start, complete these steps: 1. Connect your app to the [App Store](initial_ios) and/or [Google Play](initial-android) in the Adapty Dashboard. 2. [Create your products](create-product) in Adapty. 3. [Create a paywall and add products to it](create-paywall). 4. [Create a placement and add your paywall to it](create-placement). 5. [Install and activate the Adapty SDK](sdk-installation-kotlin-multiplatform) in your app code. :::tip The fastest way to complete these steps is to follow the [quickstart guide](quickstart). ::: ## 1. Get the paywall Your paywalls are associated with placements configured in the dashboard. Placements allow you to run different paywalls for different audiences or to run [A/B tests](ab-tests.md). To get a paywall created in the Adapty paywall builder, you need to: 1. Get the `paywall` object by the [placement](placements.md) ID using the `getPaywall` method and check whether it is a paywall created in the builder. 2. Get the paywall view configuration using the `createPaywallView` method. The view configuration contains the UI elements and styling needed to display the paywall. :::important To get the view configuration, you must switch on the **Show on device** toggle in the Paywall Builder. Otherwise, you will get an empty view configuration, and the paywall won't be displayed. ::: ```kotlin showLineNumbers Adapty.getPaywall("YOUR_PLACEMENT_ID") .onSuccess { paywall -> if (!paywall.hasViewConfiguration) { return@onSuccess } val paywallView = AdaptyUI.createPaywallView(paywall = paywall) paywallView?.present() } .onError { error -> // handle the error } ``` ## 2. Display the paywall Now, when you have the paywall configuration, it's enough to add a few lines to display your paywall. In order to display the visual paywall on the device screen, you must first configure it. To do this, call the method `AdaptyUI.createPaywallView()`: ```kotlin showLineNumbers val paywallView = AdaptyUI.createPaywallView(paywall = paywall) paywallView?.present() ``` After the view has been successfully created, you can present it on the screen of the device. :::tip For more details on how to display a paywall, see our [guide](kmp-present-paywalls.md). ::: ## 3. Handle button actions When users click buttons in the paywall, the Kotlin Multiplatform SDK automatically handles purchases, restoration, closing the paywall, and opening links. However, other buttons have custom or pre-defined IDs and require handling actions in your code. Or, you may want to override their default behavior. For example, here is the default behavior for the close button. You don't need to add it in the code, but here, you can see how it is done if needed. :::tip Read our guides on how to handle button [actions](kmp-handle-paywall-actions.md) and [events](kmp-handling-events.md). ::: ```kotlin showLineNumbers AdaptyUI.setPaywallsEventsObserver(object : AdaptyUIPaywallsEventsObserver { override fun paywallViewDidPerformAction(view: AdaptyUIPaywallView, action: AdaptyUIAction) { when (action) { AdaptyUIAction.CloseAction, AdaptyUIAction.AndroidSystemBackAction -> view.dismiss() } } }) ``` ## Next steps Your paywall is ready to be displayed in the app. Test your purchases in the [App Store sandbox](test-purchases-in-sandbox) or in [Google Play Store](testing-on-android) to make sure you can complete a test purchase from the paywall. Now, you need to [check the users' access level](kmp-check-subscription-status.md) to ensure you display a paywall or give access to paid features to right users. ## Full example Here is how all those steps can be integrated in your app together. ```kotlin showLineNumbers // Set up the observer for handling paywall actions AdaptyUI.setPaywallsEventsObserver(object : AdaptyUIPaywallsEventsObserver { override fun paywallViewDidPerformAction(view: AdaptyUIPaywallView, action: AdaptyUIAction) { when (action) { is AdaptyUIAction.CloseAction -> view.dismiss() } } }) // Get and display the paywall Adapty.getPaywall("YOUR_PLACEMENT_ID") .onSuccess { paywall -> if (!paywall.hasViewConfiguration) { // Use custom logic return@onSuccess } val paywallView = AdaptyUI.createPaywallView(paywall = paywall) paywallView?.present() } .onError { error -> // handle the error } ``` --- # File: kmp-check-subscription-status --- --- title: "Check subscription status in Kotlin Multiplatform SDK" description: "Learn how to check subscription status in your Kotlin Multiplatform app with Adapty." --- To decide whether users can access paid content or see a paywall, you need to check their [access level](access-level.md) in the profile. This article shows you how to access the profile state to decide what users need to see - whether to show them a paywall or grant access to paid features. ## Get subscription status When you decide whether to show a paywall or paid content to a user, you check their [access level](access-level.md) in their profile. You have two options: - Call `getProfile` if you need the latest profile data immediately (like on app launch) or want to force an update. - Set up **automatic profile updates** to keep a local copy that's automatically refreshed whenever the subscription status changes. ### Get profile The easiest way to get the subscription status is to use the `getProfile` method to access the profile: ```kotlin showLineNumbers Adapty.getProfile() .onSuccess { profile -> // check the access } .onError { error -> // handle the error } ``` ### Listen to subscription updates To automatically receive profile updates in your app: 1. Use `Adapty.setOnProfileUpdatedListener()` to listen for profile changes - Adapty will automatically call this method whenever the user's subscription status changes. 2. Store the updated profile data when this method is called, so you can use it throughout your app without making additional network requests. ```kotlin showLineNumbers class SubscriptionManager { private var currentProfile: AdaptyProfile? = null init { // Listen for profile updates Adapty.setOnProfileUpdatedListener { profile -> currentProfile = profile // Update UI, unlock content, etc. } } // Use stored profile instead of calling getProfile() fun hasAccess(): Boolean { return currentProfile?.accessLevels?.get("YOUR_ACCESS_LEVEL")?.isActive == true } } ``` :::note Adapty automatically calls the profile update listener when your app starts, providing cached subscription data even if the device is offline. ::: ## Connect profile with paywall logic When you need to make immediate decisions about showing paywalls or granting access to paid features, you can check the user's profile directly. This approach is useful for scenarios like app launch, when entering premium sections, or before displaying specific content. ```kotlin showLineNumbers private fun checkAccessAndShowPaywall() { // First, check if user has access Adapty.getProfile() .onSuccess { profile -> val hasAccess = profile.accessLevels?.get("YOUR_ACCESS_LEVEL")?.isActive == true if (!hasAccess) { // User doesn't have access, show paywall showPaywall() } else { // User has access, show premium content showPremiumContent() } } .onError { error -> // If we can't check access, show paywall as fallback showPaywall() } } private fun showPaywall() { // Get and display paywall using the KMP SDK Adapty.getPaywall("YOUR_PLACEMENT_ID") .onSuccess { paywall -> if (paywall.hasViewConfiguration) { val paywallView = AdaptyUI.createPaywallView(paywall = paywall) paywallView?.present() } else { // Handle remote config paywall or show custom UI handleRemoteConfigPaywall(paywall) } } .onError { error -> // Handle paywall loading error showError("Unable to load paywall") } } private fun showPremiumContent() { // Show your premium content here // This is where you unlock paid features } ``` ## Next steps Now, when you know how to track the subscription status, learn how to [work with user profiles](kmp-quickstart-identify.md) to ensure they can access what they have paid for. --- # File: kmp-quickstart-identify --- --- title: "Identify users in Kotlin Multiplatform SDK" description: "Quickstart guide to setting up Adapty for in-app subscription management in KMP." --- :::important This guide is for you if you have your own authentication system. Here, you will learn how to work with user profiles in Adapty to ensure it aligns with your existing authentication system. ::: How you manage users' purchases depends on your app's authentication model: - If your app doesn't use backend authentication and doesn't store user data, see the [section about anonymous users](#anonymous-users). - If your app has (or will have) backend authentication, see the [section about identified users](#identified-users). **Key concepts**: - **Profiles** are the entities required for the SDK to work. Adapty creates them automatically. - They can be anonymous **(without customer user ID)** or identified **(with customer user ID)**. - You provide **customer user ID** in order to cross-reference profiles in Adapty with your internal auth system Here is what is different for anonymous and identified users: | | Anonymous users | Identified users | |-------------------------|---------------------------------------------------|-------------------------------------------------------------------------| | **Purchase management** | Store-level purchase restoration | Maintain purchase history across devices through their customer user ID | | **Profile management** | New profiles on each reinstall | The same profile across sessions and devices | | **Data persistence** | Anonymous users' data is tied to app installation | Identified users' data persists across app installations | ## Anonymous users If you don't have backend authentication, **you don't need to handle authentication in the app code**: 1. When the SDK is activated on the app's first launch, Adapty **creates a new profile for the user**. 2. When the user purchases anything in the app, this purchase is **associated with their Adapty profile and their store account**. 3. When the user **re-installs** the app or installs it from a **new device**, Adapty **creates a new anonymous profile on activation**. 4. If the user has previously made purchases in your app, by default, their purchases are automatically synced from the App Store on the SDK activation. So, with anonymous users, new profiles will be created on each installation, but that's not a problem because, in the Adapty analytics, you can [configure what will be considered a new installation](general#4-installs-definition-for-analytics). For anonymous users, you need to count installs by **device IDs**. In this case, each app installation on a device is counted as an install, including reinstalls. :::note Backup restores behave differently from reinstalls. By default, when a user restores from a backup, the SDK preserves cached data and does not create a new profile. You can configure this behavior using the `withAppleClearDataOnBackup` setting. [Learn more](sdk-installation-kotlin-multiplatform#clear-data-on-backup-restore). ::: ## Identified users You have two options to identify users in the app: - [**During login/signup:**](#during-loginsignup) If users sign in after your app starts, call `identify()` with a customer user ID when they authenticate. - [**During the SDK activation:**](#during-the-sdk-activation) If you already have a customer user ID stored when the app launches, send it when calling `activate()`. :::important By default, when Adapty receives a purchase from a Customer User ID that is currently associated with another Customer User ID, the access level is shared, so both profiles have paid access. You can configure this setting to transfer paid access from one profile to another or disable sharing completely. See the [article](general#6-sharing-purchases-between-user-accounts) for more details. ::: ### During login/signup If you're identifying users after the app launch (for example, after they log into your app or sign up), use the `identify` method to set their customer user ID. - If you **haven't used this customer user ID before**, Adapty will automatically link it to the current profile. - If you **have used this customer user ID to identify the user before**, Adapty will switch to working with the profile associated with this customer user ID. :::important Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one. ::: ```kotlin showLineNumbers Adapty.identify("YOUR_USER_ID") // Unique for each user .onSuccess { // successful identify } .onError { error -> // handle the error } ``` ### During the SDK activation If you already know a customer user ID when you activate the SDK, you can send it in the `activate` method instead of calling `identify` separately. If you know a customer user ID but set it only after the activation, that will mean that, upon activation, Adapty will create a new anonymous profile and switch to the existing one only after you call `identify`. You can pass either an existing customer user ID (the one you have used before) or a new one. If you pass a new one, a new profile created upon activation will be automatically linked to the customer user ID. :::note By default, creating anonymous profiles does not affect analytics dashboards, because installs are counted based on device IDs. A device ID represents a single installation of the app from the store on a device and is regenerated only after the app is reinstalled. It does not depend on whether this is a first or repeated installation, or whether an existing customer user ID is used. Creating a profile (on SDK activation or logout), logging in, or upgrading the app without reinstalling the app does not generate additional install events. If you want to count installs based on unique users rather than devices, go to **App settings** and configure [**Installs definition for analytics**](general#4-installs-definition-for-analytics). ::: ```kotlin showLineNumbers AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withCustomerUserId("user123") // Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one. .build() ``` ### Log users out If you have a button for logging users out, use the `logout` method. :::important Logging users out creates a new anonymous profile for the user. ::: ```kotlin showLineNumbers Adapty.logout() .onSuccess { // successful logout } .onError { error -> // handle the error } ``` :::info To log users back into the app, use the `identify` method. ::: ### Allow purchases without login If your users can make purchases both before and after they log into your app, you need to ensure that they will keep access after they log in: 1. When a logged-out user makes a purchase, Adapty ties it to their anonymous profile ID. 2. When the user logs into their account, Adapty switches to working with their identified profile. - If it is a new customer user ID (e.g., the purchase has been made before registration), Adapty assigns the customer user ID to the current profile, so all the purchase history is maintained. - If it is an existing customer user ID (the customer user ID is already linked to a profile), you need to get the actual access level after the profile switch. You can either call [`getProfile`](kmp-check-subscription-status.md) right after the identification, or [listen for profile updates](kmp-check-subscription-status.md) so the data syncs automatically. ## Next steps Congratulations! You have implemented in-app payment logic in your app! We wish you all the best with your app monetization! To get even more from Adapty, you can explore these topics: - [**Testing**](troubleshooting-test-purchases.md): Ensure that everything works as expected - [**Integrations**](configuration.md): Integrate with marketing attribution and analytics services in just one line of code - [**Set custom profile attributes**](kmp-setting-user-attributes.md): Add custom attributes to user profiles and create segments, so you can launch A/B tests or show different paywalls to different users --- # File: adapty-cursor-kmp --- --- title: "Integrate Adapty into your Kotlin Multiplatform app with AI assistance" description: "A step-by-step guide to integrating Adapty into your Kotlin Multiplatform app using Cursor, Context7, ChatGPT, Claude, or other AI tools." --- This guide helps you integrate Adapty into your Kotlin Multiplatform app with the help of an LLM. You'll start by preparing your Adapty dashboard, then work through each implementation stage by sending focused doc links to your LLM. At the end, you'll find best practices for setting up your AI tools with Adapty documentation. :::tip Copy this entire page as Markdown and paste it into your LLM to get started — click **Copy for LLM** at the top of the page or open [the .md version](https://adapty.io/docs/adapty-cursor-kmp.md). The LLM will use the guide links and checkpoints to walk you through each stage. ::: ## Before you start: dashboard checklist Adapty requires dashboard configuration before you write any SDK code. Your LLM cannot look up dashboard values for you — you'll need to provide them. ### Required before coding 1. **Connect your app stores**: In the Adapty Dashboard, go to **App settings → General**. Connect both App Store and Google Play if your KMP app targets both platforms. This is required for purchases to work. [Connect app stores](integrate-payments.md) 2. **Copy your Public SDK key**: In the Adapty Dashboard, go to **App settings → General**, then find the **API keys** section. In code, this is the string you pass to the Adapty configuration builder. 3. **Create at least one product**: In the Adapty Dashboard, go to the **Products** page. You don't reference products directly in code — Adapty delivers them through paywalls. [Add products](quickstart-products.md) 4. **Create a paywall and a placement**: In the Adapty Dashboard, create a paywall on the **Paywalls** page, then assign it to a placement on the **Placements** page. In code, the placement ID is the string you pass to `Adapty.getPaywall("YOUR_PLACEMENT_ID")`. [Create paywall](quickstart-paywalls.md) 5. **Set up access levels**: In the Adapty Dashboard, configure per product on the **Products** page. In code, the string checked in `profile.accessLevels["premium"]?.isActive`. The default `premium` access level works for most apps. If paying users get access to different features depending on the product (for example, a `basic` plan vs. a `pro` plan), [create additional access levels](assigning-access-level-to-a-product.md) before you start coding. :::tip Once you have all five, you're ready to write code. Tell your LLM: "My Public SDK key is X, my placement ID is Y" so it can generate correct initialization and paywall-fetching code. ::: ### Set up when ready These are not required to start coding, but you'll want them as your integration matures: - **A/B tests**: Configure on the **Placements** page. No code change needed. [A/B tests](ab-tests.md) - **Additional paywalls and placements**: Add more `getPaywall` calls with different placement IDs. - **Analytics integrations**: Configure on the **Integrations** page. Setup varies by integration. See [analytics integrations](analytics-integration.md) and [attribution integrations](attribution-integration.md). ## Feed Adapty docs to your LLM ### Use Context7 (recommended) [Context7](https://context7.com) is an MCP server that gives your LLM direct access to up-to-date Adapty documentation. Your LLM fetches the right docs automatically based on what you ask — no manual URL pasting needed. Context7 works with **Cursor**, **Claude Code**, **Windsurf**, and other MCP-compatible tools. To set it up, run: ``` npx ctx7 setup ``` This detects your editor and configures the Context7 server. For manual setup, see the [Context7 GitHub repository](https://github.com/upstash/context7). Once configured, reference the Adapty library in your prompts: ``` Use the adaptyteam/adapty-docs library to look up how to install the Kotlin Multiplatform SDK ``` :::warning Even though Context7 removes the need to paste doc links manually, the implementation order matters. Follow the [implementation walkthrough](#implementation-walkthrough) below step by step to make sure everything works. ::: ### Use plain text docs You can access any Adapty doc as plain text Markdown. Add `.md` to the end of its URL, or click **Copy for LLM** under the article title. For example: [adapty-cursor-kmp.md](https://adapty.io/docs/adapty-cursor-kmp.md). Each stage in the [implementation walkthrough](#implementation-walkthrough) below includes a "Send this to your LLM" block with `.md` links to paste. For more documentation at once, see [index files and platform-specific subsets](#plain-text-doc-index-files) below. ## Implementation walkthrough The rest of this guide walks through Adapty integration in implementation order. Each stage includes the docs to send to your LLM, what you should see when done, and common issues. ### Plan your integration Before jumping into code, ask your LLM to analyze your project and create an implementation plan. If your AI tool supports a planning mode (like Cursor's or Claude Code's plan mode), use it so the LLM can read both your project structure and the Adapty docs before writing any code. Tell your LLM which approach you use for purchases — this affects the guides it should follow: - [**Adapty Paywall Builder**](adapty-paywall-builder.md): You create paywalls in Adapty's no-code builder, and the SDK renders them automatically. - [**Manually created paywalls**](kmp-making-purchases.md): You build your own paywall UI in code but still use Adapty to fetch products and handle purchases. - [**Observer mode**](observer-vs-full-mode.md): You keep your existing purchase infrastructure and use Adapty only for analytics and integrations. Not sure which one to pick? Read the [comparison table in the quickstart](kmp-quickstart-paywalls.md). ### Install and configure the SDK Add the Adapty SDK dependency via Gradle and activate it with your Public SDK key. This is the foundation — nothing else works without it. **Guide:** [Install & configure Adapty SDK](sdk-installation-kotlin-multiplatform.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/sdk-installation-kotlin-multiplatform.md ``` :::tip[Checkpoint] - **Expected:** App builds and runs. Logcat (Android) or Xcode console (iOS) shows Adapty activation log. - **Gotcha:** "Public API key is missing" → check you replaced the placeholder with your real key from App settings. ::: ### Show paywalls and handle purchases Fetch a paywall by placement ID, display it, and handle purchase events. The guides you need depend on how you handle purchases. Test each purchase in the sandbox as you go — don't wait until the end. See [Test purchases in sandbox](test-purchases-in-sandbox.md) for setup instructions. **Guides:** - [Enable purchases using paywalls (quickstart)](kmp-quickstart-paywalls.md) - [Fetch Paywall Builder paywalls and their configuration](kmp-get-pb-paywalls.md) - [Display paywalls](kmp-present-paywalls.md) - [Handle paywall events](kmp-handling-events.md) - [Respond to button actions](kmp-handle-paywall-actions.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/kmp-quickstart-paywalls.md - https://adapty.io/docs/kmp-get-pb-paywalls.md - https://adapty.io/docs/kmp-present-paywalls.md - https://adapty.io/docs/kmp-handling-events.md - https://adapty.io/docs/kmp-handle-paywall-actions.md ``` :::tip[Checkpoint] - **Expected:** Paywall appears with your configured products. Tapping a product triggers the sandbox purchase dialog. - **Gotcha:** Empty paywall or `getPaywall` error → verify placement ID matches the dashboard exactly and the placement has an audience assigned. ::: **Guides:** - [Enable purchases in your custom paywall (quickstart)](kmp-quickstart-manual.md) - [Fetch paywalls and products](fetch-paywalls-and-products-kmp.md) - [Render paywall designed by remote config](present-remote-config-paywalls-kmp.md) - [Make purchases](kmp-making-purchases.md) - [Restore purchases](kmp-restore-purchase.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/kmp-quickstart-manual.md - https://adapty.io/docs/fetch-paywalls-and-products-kmp.md - https://adapty.io/docs/present-remote-config-paywalls-kmp.md - https://adapty.io/docs/kmp-making-purchases.md - https://adapty.io/docs/kmp-restore-purchase.md ``` :::tip[Checkpoint] - **Expected:** Your custom paywall displays products fetched from Adapty. Tapping a product triggers the sandbox purchase dialog. - **Gotcha:** Empty products array → verify the paywall has products assigned in the dashboard and the placement has an audience. ::: **Guides:** - [Observer mode overview](observer-vs-full-mode.md) - [Implement Observer mode](implement-observer-mode-kmp.md) - [Report transactions in Observer mode](report-transactions-observer-mode-kmp.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/observer-vs-full-mode.md - https://adapty.io/docs/implement-observer-mode-kmp.md - https://adapty.io/docs/report-transactions-observer-mode-kmp.md ``` :::tip[Checkpoint] - **Expected:** After a sandbox purchase using your existing purchase flow, the transaction appears in the Adapty dashboard **Event Feed**. - **Gotcha:** No events → verify you're reporting transactions to Adapty and server notifications are configured for both stores. ::: ### Check subscription status After a purchase, check the user profile for an active access level to gate premium content. **Guide:** [Check subscription status](kmp-check-subscription-status.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/kmp-check-subscription-status.md ``` :::tip[Checkpoint] - **Expected:** After a sandbox purchase, `profile.accessLevels["premium"]?.isActive` returns `true`. - **Gotcha:** Empty `accessLevels` after purchase → check the product has an access level assigned in the dashboard. ::: ### Identify users Link your app user accounts to Adapty profiles so purchases persist across devices. :::important Skip this step if your app has no authentication. ::: **Guide:** [Identify users](kmp-quickstart-identify.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/kmp-quickstart-identify.md ``` :::tip[Checkpoint] - **Expected:** After calling `Adapty.identify("your-user-id")`, the dashboard **Profiles** section shows your custom user ID. - **Gotcha:** Call `identify` after activation but before fetching paywalls to avoid anonymous profile attribution. ::: ### Prepare for release Once your integration works in the sandbox, walk through the release checklist to make sure everything is production-ready. **Guide:** [Release checklist](release-checklist.md) Send this to your LLM: ``` Read these Adapty docs before releasing: - https://adapty.io/docs/release-checklist.md ``` :::tip[Checkpoint] - **Expected:** All checklist items confirmed: store connections, server notifications, purchase flow, access level checks, and privacy requirements. - **Gotcha:** Missing server notifications → configure App Store Server Notifications in **App settings → iOS SDK** and Google Play Real-Time Developer Notifications in **App settings → Android SDK**. ::: ## Plain text doc index files If you need to give your LLM broader context beyond individual pages, we host index files that list or combine all Adapty documentation: - [`llms.txt`](https://adapty.io/docs/llms.txt): Lists all pages with `.md` links. An [emerging standard](https://llmstxt.org/) for making websites accessible to LLMs. Note that for some AI agents (e.g., ChatGPT) you will need to download `llms.txt` and upload it to the chat as a file. - [`llms-full.txt`](https://adapty.io/docs/llms-full.txt): The entire Adapty documentation site combined into a single file. Very large — use only when you need the full picture. - Kotlin Multiplatform-specific [`kmp-llms.txt`](https://adapty.io/docs/kmp-llms.txt) and [`kmp-llms-full.txt`](https://adapty.io/docs/kmp-llms-full.txt): Platform-specific subsets that save tokens compared to the full site. --- # File: kmp-paywalls --- --- title: "Paywalls in Kotlin Multiplatform SDK" description: "Learn how to work with paywalls in your Kotlin Multiplatform app with Adapty SDK." --- ## Display paywalls ### Adapty Paywall Builder :::tip To get started with the Adapty Paywall Builder paywalls quickly, see our [quickstart guide](kmp-quickstart-paywalls). ::: ### Implement paywalls manually For more guides on implementing paywalls and handling purchases manually, see the [category](kmp-implement-paywalls-manually). ## Useful features --- # File: kmp-get-pb-paywalls --- --- title: "Fetch Paywall Builder paywalls and their configuration in Kotlin Multiplatform SDK" description: "Learn how to retrieve PB paywalls in Adapty for better subscription control in your Kotlin Multiplatform app." --- After [you designed the visual part for your paywall](adapty-paywall-builder) with the new Paywall Builder in the Adapty Dashboard, you can display it in your mobile app. The first step in this process is to get the paywall associated with the placement and its view configuration as described below. Please be aware that this topic refers to Paywall Builder-customized paywalls. If you are implementing your paywalls manually, please refer to the [Fetch paywalls and products for remote config paywalls in your mobile app](fetch-paywalls-and-products-kmp) topic. :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. :::
Before you start displaying paywalls in your mobile app (click to expand) 1. [Create your products](create-product) in the Adapty Dashboard. 2. [Create a paywall and incorporate the products into it](create-paywall) in the Adapty Dashboard. 3. [Create placements and incorporate your paywall into it](create-placement) in the Adapty Dashboard. 4. Install [Adapty SDK](sdk-installation-kotlin-multiplatform) in your mobile app.
## Fetch paywall designed with Paywall Builder If you've [designed a paywall using the Paywall Builder](adapty-paywall-builder), you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. Nevertheless, you need to get its ID via the placement, its view configuration, and then present it in your mobile app. To ensure optimal performance, it's crucial to retrieve the paywall and its [view configuration](kmp-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) as early as possible, allowing sufficient time for images to download before presenting them to the user. To get a paywall, use the `getPaywall` method: ```kotlin showLineNumbers Adapty.getPaywall( placementId = "YOUR_PLACEMENT_ID", locale = "en", fetchPolicy = AdaptyPaywallFetchPolicy.Default, loadTimeout = 5.seconds ).onSuccess { paywall -> // the requested paywall }.onError { error -> // handle the error } ``` Parameters: | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the [paywall localization](add-paywall-locale-in-adapty-paywall-builder). This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.

| | **fetchPolicy** | default: `AdaptyPaywallFetchPolicy.Default` |

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `AdaptyPaywallFetchPolicy.ReturnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

Adapty SDK stores paywalls locally in two layers: regularly updated cache described above and [fallback paywalls](fallback-paywalls). We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.

| | **loadTimeout** | default: 5 sec |

This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.

Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.

For Kotlin Multiplatform: You can create `TimeInterval` with extension functions (like `5.seconds`, where `.seconds` is from `import com.adapty.utils.seconds`), or `TimeInterval.seconds(5)`. To set no limitation, use `TimeInterval.INFINITE`.

| Response parameters: | Parameter | Description | | :-------- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------| | Paywall | An [`AdaptyPaywall`](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-paywall/) object with a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch the view configuration of paywall designed using Paywall Builder :::important Make sure to enable the **Show on device** toggle in the paywall builder. If this option isn't turned on, the view configuration won't be available to retrieve. ::: After fetching the paywall, check if it includes a `ViewConfiguration`, which indicates that it was created using Paywall Builder. This will guide you on how to display the paywall. If the `ViewConfiguration` is present, treat it as a Paywall Builder paywall; if not, [handle it as a remote config paywall](present-remote-config-paywalls-kmp). Use the `createPaywallView` method to load the view configuration. ```kotlin showLineNumbers if (paywall.hasViewConfiguration) { AdaptyUI.createPaywallView( paywall = paywall, loadTimeout = 5.seconds, preloadProducts = true ).onSuccess { paywallView -> // use paywallView }.onError { error -> // handle the error } } else { // use your custom logic } ``` | Parameter | Presence | Description | | :--------------------------- | :------------- | :----------------------------------------------------------- | | **paywall** | required | An `AdaptyPaywall` object to obtain a controller for the desired paywall. | | **loadTimeout** | optional | This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned. Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood. You can use extension functions like `5.seconds` from `kotlin.time.Duration.Companion`. | | **preloadProducts** | optional | Set to `true` to preload products for better performance. When enabled, products are loaded in advance, reducing the time needed to display the paywall. | | **productPurchaseParams** | optional | A map of [`AdaptyProductIdentifier`](https://kmp.adapty.io/adapty/com.adapty.kmp.models/-adapty-product-identifier/) to [`AdaptyPurchaseParameters`](https://kmp.adapty.io/adapty/com.adapty.kmp.models/-adapty-purchase-parameters/). Use this to configure purchase-specific parameters like personalized offers or subscription update parameters for individual products in the paywall. | :::note If you are using multiple languages, learn how to add a [Paywall Builder localization](add-paywall-locale-in-adapty-paywall-builder). ::: Once loaded, [present the paywall](kmp-present-paywalls). ## Get a paywall for a default audience to fetch it faster Typically, paywalls are fetched almost instantly, so you don't need to worry about speeding up this process. However, in cases where you have numerous audiences and paywalls, and your users have a weak internet connection, fetching a paywall may take longer than you'd like. In such situations, you might want to display a default paywall to ensure a smooth user experience rather than showing no paywall at all. To address this, you can use the `getPaywallForDefaultAudience` method, which fetches the paywall of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the paywall by the `getPaywall` method, as detailed in the [Fetch Paywall Information](#fetch-paywall-designed-with-paywall-builder) section above. :::warning Why we recommend using `getPaywall` The `getPaywallForDefaultAudience` method comes with a few significant drawbacks: - **Potential backward compatibility issues**: If you need to show different paywalls for different app versions (current and future), you may face challenges. You'll either have to design paywalls that support the current (legacy) version or accept that users with the current (legacy) version might encounter issues with non-rendered paywalls. - **Loss of targeting**: All users will see the same paywall designed for the **All Users** audience, which means you lose personalized targeting (including based on countries, marketing attribution or your own custom attributes). If you're willing to accept these drawbacks to benefit from faster paywall fetching, use the `getPaywallForDefaultAudience` method as follows. Otherwise stick to `getPaywall` described [above](#fetch-paywall-designed-with-paywall-builder). ::: ```kotlin showLineNumbers Adapty.getPaywallForDefaultAudience( placementId = "YOUR_PLACEMENT_ID", locale = "en", fetchPolicy = AdaptyPaywallFetchPolicy.Default, ).onSuccess { paywall -> // the requested paywall }.onError { error -> // handle the error } ``` | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the [Placement](placements). This is the value you specified when creating a placement in your Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.

| | **fetchPolicy** | default: `AdaptyPaywallFetchPolicy.Default` |

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `AdaptyPaywallFetchPolicy.ReturnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

| ## Customize assets To customize images and videos in your paywall, implement the custom assets. Hero images and videos have predefined IDs: `hero_image` and `hero_video`. In a custom asset bundle, you target these elements by their IDs and customize their behavior. For other images and videos, you need to [set a custom ID](https://adapty.io/docs/custom-media) in the Adapty dashboard. For example, you can: - Show a different image or video to some users. - Show a local preview image while a remote main image is loading. - Show a preview image before running a video. :::important To use this feature, update the Adapty SDK to version 3.7.0 or higher. ::: Here's an example of how you can provide custom assets via a map: :::info The Kotlin Multiplatform SDK supports local assets only. For remote content, you should download and cache assets locally before using them in custom assets. ::: ```kotlin showLineNumbers // Import generated Res class for accessing resources viewModelScope.launch { // Get URIs for bundled resources using Res.getUri() val heroImagePath = Res.getUri("files/images/hero_image.png") val demoVideoPath = Res.getUri("files/videos/demo_video.mp4") // Or read image as byte data val imageByteData = Res.readBytes("files/images/avatar.png") // Create custom assets map val customAssets: Map = mapOf( // Load image from app resources (bundled with the app) // Files should be placed in commonMain/composeResources/files/ "hero_image" to AdaptyCustomAsset.localImageResource( path = heroImagePath ), // Or use image byte data "avatar" to AdaptyCustomAsset.localImageData( data = imageByteData ), // Load video from app resources "demo_video" to AdaptyCustomAsset.localVideoResource( path = demoVideoPath ), // Or use a video file from device storage "intro_video" to AdaptyCustomAsset.localVideoFile( path = "/path/to/local/video.mp4" ), // Apply custom brand colors "brand_primary" to AdaptyCustomAsset.color( colorHex = "#FF6B35" ), // Create gradient background "card_gradient" to AdaptyCustomAsset.linearGradient( colors = listOf("#1E3A8A", "#3B82F6", "#60A5FA"), stops = listOf(0.0f, 0.5f, 1.0f) ) ) // Use custom assets when creating paywall view AdaptyUI.createPaywallView( paywall = paywall, customAssets = customAssets ).onSuccess { paywallView -> // Present the paywall with custom assets paywallView.present() }.onError { error -> // Handle the error - paywall will fall back to default appearance } } ``` :::note If an asset is not found or fails to load, the paywall will fall back to its default appearance configured in the Paywall Builder. ::: --- # File: kmp-present-paywalls --- --- title: "Kotlin Multiplatform - Present new Paywall Builder paywalls" description: "Learn how to present paywalls on Kotlin Multiplatform for effective monetization." --- If you've customized a paywall using the Paywall Builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. :::warning This guide is for **new Paywall Builder paywalls** only. The process for presenting paywalls differs for paywalls designed with remote config paywalls and [Observer mode](observer-vs-full-mode). For presenting **Remote config paywalls**, see [Render paywall designed by remote config](present-remote-config-paywalls-kmp). ::: To display a paywall, use the `view.present()` method on the `view` created by the [`createPaywallView`](kmp-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) method. Each `view` can only be used once. If you need to display the paywall again, call `createPaywallView` one more to create a new `view` instance. :::warning Reusing the same `view` without recreating it may result in an error. ::: ```kotlin showLineNumbers title="Kotlin Multiplatform" viewModelScope.launch { AdaptyUI.createPaywallView(paywall = paywall).onSuccess { view -> view.present() }.onError { error -> // handle the error } } ``` ## Custom tags Custom tags let you avoid creating separate paywalls for different scenarios. Imagine a single paywall that adapts dynamically based on user data. For example, instead of a generic "Hello!", you could greet users personally with "Hello, John!" or "Hello, Ann!" Here are some ways you can use custom tags: - Display the user's name or email on the paywall. - Show the current day of the week to boost sales (e.g., "Happy Thursday"). - Add personalized details about the products you're selling (like the name of a fitness program or a phone number in a VoIP app). Custom tags help you create a flexible paywall that adapts to various situations, making your app's interface more personalized and engaging. :::warning Make sure to add fallbacks for every line with custom tags. Remember to include fallbacks for every line with custom tags. In some cases, your app might not know what to replace a custom tag with—especially if users are on an older version of the AdaptyUI SDK. To prevent this, always add fallback text that will replace lines containing unknown custom tags. Without this, users might see the tags displayed as code (``). ::: To use custom tags in your paywall, pass them when creating the paywall view: ```kotlin showLineNumbers viewModelScope.launch { val customTags = mapOf( "USERNAME" to "John", "DAY_OF_WEEK" to "Thursday" ) AdaptyUI.createPaywallView( paywall = paywall, customTags = customTags ).onSuccess { view -> view.present() }.onError { error -> // handle the error } } ``` ## Custom timers The paywall timer is a great tool for promoting special and seasonal offers with a time limit. However, it's important to note that this timer isn't connected to the offer's validity or the campaign's duration. It's simply a standalone countdown that starts from the value you set and decreases to zero. When the timer reaches zero, nothing happens—it just stays at zero. You can customize the text before and after the timer to create the desired message, such as: "Offer ends in: 10:00 sec." To use custom timers in your paywall, pass them when creating the paywall view: ```kotlin showLineNumbers viewModelScope.launch { val customTimers = mapOf( "CUSTOM_TIMER_NY" to LocalDateTime(2025, 1, 1, 0, 0, 0), "CUSTOM_TIMER_SALE" to LocalDateTime(2024, 12, 31, 23, 59, 59) ) AdaptyUI.createPaywallView( paywall = paywall, customTimers = customTimers ).onSuccess { view -> view.present() }.onError { error -> // handle the error } } ``` ## Show dialog Use this method instead of native alert dialogs when a paywall view is presented on Android. On Android, regular alerts appear behind the paywall view, which makes them invisible to users. This method ensures proper dialog presentation above the paywall on all platforms. ```kotlin showLineNumbers title="Kotlin Multiplatform" viewModelScope.launch { view.showDialog( title = "Close paywall?", content = "You will lose access to exclusive offers.", primaryActionTitle = "Stay", secondaryActionTitle = "Close" ).onSuccess { action -> if (action == AdaptyUIDialogActionType.SECONDARY) { // User confirmed - close the paywall view.dismiss() } // If primary - do nothing, user stays }.onError { error -> // handle the error } } ``` ## Configure iOS presentation style Configure how the paywall is presented on iOS by passing the `iosPresentationStyle` parameter to the `present()` method. The parameter accepts `AdaptyUIIOSPresentationStyle.FULLSCREEN` (default) or `AdaptyUIIOSPresentationStyle.PAGESHEET` values. ```kotlin showLineNumbers viewModelScope.launch { val view = AdaptyUI.createPaywallView(paywall = paywall).getOrNull() view?.present(iosPresentationStyle = AdaptyUIIOSPresentationStyle.PAGESHEET) } ``` --- # File: kmp-handle-paywall-actions --- --- title: "Respond to button actions in Kotlin Multiplatform SDK" description: "Handle paywall button actions in Kotlin Multiplatform using Adapty for better app monetization." --- :::warning **Only purchases and restorations are handled automatically.** All the other button actions, such as closing paywalls or opening links, require implementing proper responses in the app code. ::: If you are building paywalls using the Adapty paywall builder, it's crucial to set up buttons properly: 1. Add a [button in the paywall builder](paywall-buttons.md) and assign it either a pre-existing action or create a custom action ID. 2. Write code in your app to handle each action you've assigned. This guide shows how to handle custom and pre-existing actions in your code. ## Set up the AdaptyUIPaywallsEventsObserver To handle paywall actions, you need to implement the `AdaptyUIPaywallsEventsObserver` interface and set it up with `AdaptyUI.setPaywallsEventsObserver()`. This should be done early in your app's lifecycle, typically in your main activity or app initialization. ```kotlin // In your app initialization AdaptyUI.setPaywallsEventsObserver(MyAdaptyUIPaywallsEventsObserver()) ``` ## Close paywalls To add a button that will close your paywall: 1. In the paywall builder, add a button and assign it the **Close** action. 2. In your app code, implement a handler for the `close` action that dismisses the paywall. :::info In the Kotlin Multiplatform SDK, the `CloseAction` and `AndroidSystemBackAction` trigger closing the paywall by default. However, you can override this behavior in your code if needed. For example, closing one paywall might trigger opening another. ::: ```kotlin class MyAdaptyUIPaywallsEventsObserver : AdaptyUIPaywallsEventsObserver { override fun paywallViewDidPerformAction(view: AdaptyUIPaywallView, action: AdaptyUIAction) { when (action) { AdaptyUIAction.CloseAction, AdaptyUIAction.AndroidSystemBackAction -> view.dismiss() } } } // Set up the observer AdaptyUI.setPaywallsEventsObserver(MyAdaptyUIPaywallsEventsObserver()) ``` ## Open URLs from paywalls :::tip If you want to add a group of links (e.g., terms of use and purchase restoration), add a **Link** element in the paywall builder and handle it the same way as buttons with the **Open URL** action. ::: To add a button that opens a link from your paywall (e.g., **Terms of use** or **Privacy policy**): 1. In the paywall builder, add a button, assign it the **Open URL** action, and enter the URL you want to open. 2. In your app code, implement a handler for the `openUrl` action that opens the received URL in a browser. :::info In the Kotlin Multiplatform SDK, the `OpenUrlAction` provides the URL that should be opened. You can implement custom logic to handle URL opening, such as showing a confirmation dialog or using your app's preferred URL handling method. ::: ```kotlin class MyAdaptyUIPaywallsEventsObserver( private val uriHandler: UriHandler ) : AdaptyUIPaywallsEventsObserver { override fun paywallViewDidPerformAction(view: AdaptyUIPaywallView, action: AdaptyUIAction) { when (action) { is AdaptyUIAction.OpenUrlAction -> { // Show confirmation dialog before opening URL mainUiScope.launch { val selectedAction = view.showDialog( title = "Open URL?", content = action.url, primaryActionTitle = "Cancel", secondaryActionTitle = "Open" ).getOrNull() when (selectedAction) { AdaptyUIDialogActionType.PRIMARY -> { // User cancelled } AdaptyUIDialogActionType.SECONDARY -> { // User confirmed - open URL uriHandler.openUri(action.url) } else -> Unit } } } } } } // Set up the observer with UriHandler AdaptyUI.setPaywallsEventsObserver(MyAdaptyUIPaywallsEventsObserver(uriHandler)) ``` ## Log into the app To add a button that logs users into your app: 1. In the paywall builder, add a button and assign it a **Custom** action with the ID "login". 2. In your app code, implement a handler for the custom action that identifies your user. ```kotlin class MyAdaptyUIObserver : AdaptyUIObserver { override fun paywallViewDidPerformAction(view: AdaptyUIView, action: AdaptyUIAction) { when (action) { is AdaptyUIAction.CustomAction -> { if (action.action == "login") { // Handle login action - navigate to login screen // This depends on your app's navigation system // For example, in Compose Multiplatform: // navController.navigate("login") } } } } } ``` ## Handle custom actions To add a button that handles any other actions: 1. In the paywall builder, add a button, assign it the **Custom** action, and assign it an ID. 2. In your app code, implement a handler for the action ID you've created. For example, if you have another set of subscription offers or one-time purchases, you can add a button that will display another paywall: ```kotlin class MyAdaptyUIPaywallsEventsObserver : AdaptyUIPaywallsEventsObserver { override fun paywallViewDidPerformAction(view: AdaptyUIPaywallView, action: AdaptyUIAction) { when (action) { is AdaptyUIAction.CustomAction -> { when (action.action) { "login" -> { // Handle login action - navigate to login screen // This depends on your app's navigation system // For example, in Compose Multiplatform: // navController.navigate("login") } } } } } } // Set up the observer AdaptyUI.setPaywallsEventsObserver(MyAdaptyUIPaywallsEventsObserver()) ``` --- # File: kmp-handling-events --- --- title: "Kotlin Multiplatform - Handle paywall events" description: "Handle Kotlin Multiplatform subscription events efficiently with Adapty's event tracking tools." --- Paywalls configured with the [Paywall Builder](adapty-paywall-builder) don't need extra code to make and restore purchases. However, they generate some events that your app can respond to. Those events include button presses (close buttons, URLs, product selections, and so on) as well as notifications on purchase-related actions taken on the paywall. Learn how to respond to these events below. :::warning This guide is for **new Paywall Builder paywalls** only. ::: To control or monitor processes occurring on the paywall screen within your mobile app, implement the `AdaptyUIPaywallsEventsObserver` interface methods. Some methods have default implementations that handle common scenarios automatically. :::note **Implementation Notes**: These methods are where you add your custom logic to respond to paywall events. You can use `view.dismiss()` to close the paywall, or implement any other custom behavior you need. ::: ## User-generated events ### Paywall appearance and disappearance When a paywall appears or disappears, these methods will be invoked: ```kotlin showLineNumbers title="Kotlin" override fun paywallViewDidAppear(view: AdaptyUIPaywallView) { // Handle paywall appearance // You can track analytics or update UI here } override fun paywallViewDidDisappear(view: AdaptyUIPaywallView) { // Handle paywall disappearance // You can track analytics or update UI here } ``` :::note - On iOS, `paywallViewDidAppear` is also invoked when a user taps the [web paywall button](web-paywall#step-2a-add-a-web-purchase-button) inside a paywall, and a web paywall opens in an in-app browser. - On iOS, `paywallViewDidDisappear` is also invoked when a [web paywall](web-paywall#step-2a-add-a-web-purchase-button) opened from a paywall in an in-app browser disappears from the screen. :::
Event examples (Click to expand) ```javascript // Paywall appeared { // No additional data } // Paywall disappeared { // No additional data } ```
### Product selection If a user selects a product for purchase, this method will be invoked: ```kotlin showLineNumbers title="Kotlin" override fun paywallViewDidSelectProduct(view: AdaptyUIPaywallView, productId: String) { // Handle product selection // You can update UI or track analytics here } ```
Event example (Click to expand) ```javascript { "productId": "premium_monthly" } ```
### Started purchase If a user initiates the purchase process, this method will be invoked: ```kotlin showLineNumbers title="Kotlin" override fun paywallViewDidStartPurchase(view: AdaptyUIPaywallView, product: AdaptyPaywallProduct) { // Handle purchase start // You can show loading indicators or track analytics here } ```
Event example (Click to expand) ```javascript { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } ```
### Successful, canceled, or pending purchase If a purchase succeeds, this method will be invoked. By default, it automatically dismisses the paywall unless the purchase was canceled by the user: ```kotlin showLineNumbers title="Kotlin" override fun paywallViewDidFinishPurchase( view: AdaptyUIPaywallView, product: AdaptyPaywallProduct, purchaseResult: AdaptyPurchaseResult ) { when (purchaseResult) { is AdaptyPurchaseResult.Success -> { // Check if user has access to premium features if (purchaseResult.profile.accessLevels["premium"]?.isActive == true) { view.dismiss() } } AdaptyPurchaseResult.Pending -> { // Handle pending purchase (e.g., user will pay offline with cash) } AdaptyPurchaseResult.UserCanceled -> { // Handle user cancellation } } } ```
Event examples (Click to expand) ```javascript // Successful purchase { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "purchaseResult": { "type": "Success", "profile": { "accessLevels": { "premium": { "id": "premium", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } } } } } // Pending purchase { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "purchaseResult": { "type": "Pending" } } // User canceled purchase { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "purchaseResult": { "type": "UserCanceled" } } ```
We recommend dismissing the paywall screen in case of successful purchase. ### Failed purchase If a purchase fails due to an error, this method will be invoked. This includes StoreKit/Google Play Billing errors (payment restrictions, invalid products, network failures), transaction verification failures, and system errors. Note that user cancellations trigger `paywallViewDidFinishPurchase` with a cancelled result instead, and pending payments do not trigger this method. ```kotlin showLineNumbers title="Kotlin" override fun paywallViewDidFailPurchase( view: AdaptyUIPaywallView, product: AdaptyPaywallProduct, error: AdaptyError ) { // Add your purchase failure handling logic here // For example: show error message, retry option, or custom error handling } ```
Event example (Click to expand) ```javascript { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "error": { "code": "purchase_failed", "message": "Purchase failed due to insufficient funds", "details": { "underlyingError": "Insufficient funds in account" } } } ```
### Started restore If a user initiates the restore process, this method will be invoked: ```kotlin showLineNumbers title="Kotlin" override fun paywallViewDidStartRestore(view: AdaptyUIPaywallView) { // Handle restore start // You can show loading indicators or track analytics here } ``` ### Successful restore If restoring a purchase succeeds, this method will be invoked: ```kotlin showLineNumbers title="Kotlin" override fun paywallViewDidFinishRestore(view: AdaptyUIPaywallView, profile: AdaptyProfile) { // Add your successful restore handling logic here // For example: show success message, update UI, or dismiss paywall // Check if user has access to premium features if (profile.accessLevels["premium"]?.isActive == true) { view.dismiss() } } ```
Event example (Click to expand) ```javascript { "profile": { "accessLevels": { "premium": { "id": "premium", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } }, "subscriptions": [ { "vendorProductId": "premium_monthly", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } ] } } ```
We recommend dismissing the screen if the user has the required `accessLevel`. Refer to the [Subscription status](subscription-status) topic to learn how to check it. ### Failed restore If `Adapty.restorePurchases()` fails, this method will be invoked: ```kotlin showLineNumbers title="Kotlin" override fun paywallViewDidFailRestore(view: AdaptyUIPaywallView, error: AdaptyError) { // Add your restore failure handling logic here // For example: show error message, retry option, or custom error handling } ```
Event example (Click to expand) ```javascript { "error": { "code": "restore_failed", "message": "Purchase restoration failed", "details": { "underlyingError": "No previous purchases found" } } } ```
### Web payment navigation completion If a user initiates the purchase process using a [web paywall](web-paywall.md), this method will be invoked: ```kotlin showLineNumbers title="Kotlin" override fun paywallViewDidFinishWebPaymentNavigation( view: AdaptyUIPaywallView, product: AdaptyPaywallProduct?, error: AdaptyError? ) { if (error != null) { // Handle web payment navigation error } else { // Handle successful web payment navigation } } ```
Event examples (Click to expand) ```javascript // Successful web payment navigation { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "error": null } // Failed web payment navigation { "product": null, "error": { "code": "web_payment_failed", "message": "Web payment navigation failed", "details": { "underlyingError": "Network connection error" } } } ```
## Data fetching and rendering ### Product loading errors If you don't pass the products during the initialization, AdaptyUI will retrieve the necessary objects from the server by itself. If this operation fails, AdaptyUI will report the error by calling this method: ```kotlin showLineNumbers title="Kotlin" override fun paywallViewDidFailLoadingProducts(view: AdaptyUIPaywallView, error: AdaptyError) { // Add your product loading failure handling logic here // For example: show error message, retry option, or custom error handling } ```
Event example (Click to expand) ```javascript { "error": { "code": "products_loading_failed", "message": "Failed to load products from the server", "details": { "underlyingError": "Network timeout" } } } ```
### Rendering errors If an error occurs during the interface rendering, it will be reported by this method: ```kotlin showLineNumbers title="Kotlin" override fun paywallViewDidFailRendering(view: AdaptyUIPaywallView, error: AdaptyError) { // Handle rendering error // In a normal situation, such errors should not occur // If you come across one, please let us know } ```
Event example (Click to expand) ```javascript { "error": { "code": "rendering_failed", "message": "Failed to render paywall interface", "details": { "underlyingError": "Invalid paywall configuration" } } } ```
In a normal situation, such errors should not occur, so if you come across one, please let us know. --- # File: kmp-use-fallback-paywalls --- --- title: "Kotlin Multiplatform - Use fallback paywalls" description: "Handle cases when users are offline or Adapty servers aren't available" --- To maintain a fluid user experience, it is important to set up [fallbacks](/fallback-paywalls) for your [paywalls](paywalls) and [onboardings](onboardings). This precaution extends the application's capabilities in case of partial or complete loss of internet connection. * **If the application cannot access Adapty servers:** It will be able to display a fallback paywall, and access the local onboarding configuration. * **If the application cannot access the internet:** It will be able to display a fallback paywall. Onboardings include remote content and require an internet connection to function. :::important Before you follow the steps in this guide, [download](/local-fallback-paywalls) the fallback configuration files from Adapty. ::: ## Configuration 1. Add the fallback configuration file to your application. * If your target platform is Android, move the fallback configuration file to the `android/app/src/main/assets/` folder. * If your target platform is iOS, add the fallback JSON file to your project bundle. (**File** -> **Add Files to YourProjectName**) 2. Call the `.setFallback` method **before** you fetch the target paywall or onboarding. 3. Set the `assetId` parameter, depending on your target platform. * Android: Use the file path relative to the `assets` directory. * iOS: Use the complete filename. ```kotlin showLineNumbers Adapty.setFallback(assetId = "fallback.json") .onSuccess { // Fallback paywalls loaded successfully } .onError { error -> // Handle the error } ``` Parameters: | Parameter | Description | | :---------- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **assetId** | Fallback configuration filename (iOS).
Fallback configuration file path, relative to the `assets` directory (Android). | :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: --- # File: kmp-web-paywalls --- --- title: "Implement web paywalls in Kotlin Multiplatform SDK" description: "Set up a web paywall to get paid without the store fees and audits." --- :::important Before you begin, make sure you have [configured your web paywall in the dashboard](web-paywall.md) and installed Adapty SDK version 3.15 or later. ::: ## Open web paywalls If you are working with a paywall you developed yourself, you need to handle web paywalls using the SDK method. The `openWebPaywall` method: 1. Generates a unique URL allowing Adapty to link a specific paywall shown to a particular user to the web page they are redirected to. 2. Tracks when your users return to the app and then requests `getProfile` at short intervals to determine whether the profile access rights have been updated. This way, if the payment has been successful and access rights have been updated, the subscription activates in the app almost immediately. :::note After users return to the app, refresh the UI to reflect the profile updates. Adapty will receive and process profile update events. ::: ```kotlin showLineNumbers viewModelScope.launch { Adapty.openWebPaywall(product = product).onSuccess { // the web paywall was opened successfully }.onError { error -> // handle the error } } ``` :::note There are two versions of the `openWebPaywall` method: 1. `openWebPaywall(product = product)` that generates URLs by paywall and adds the product data to URLs as well. 2. `openWebPaywall(paywall = paywall)` that generates URLs by paywall without adding the product data to URLs. Use it when your products in the Adapty paywall differ from those in the web paywall. ::: ## Open web paywalls in an in-app browser By default, web paywalls open in the external browser. To provide a seamless user experience, you can open web paywalls in an in-app browser. This displays the web purchase page within your application, allowing users to complete transactions without switching apps. To enable this, set the `openIn` parameter to `AdaptyWebPresentation.IN_APP_BROWSER`: ```kotlin showLineNumbers viewModelScope.launch { Adapty.openWebPaywall( product = product, openIn = AdaptyWebPresentation.IN_APP_BROWSER // default – EXTERNAL_BROWSER ).onSuccess { // the web paywall was opened successfully }.onError { error -> // handle the error } } ``` --- # File: kmp-troubleshoot-paywall-builder --- --- title: "Troubleshoot Paywall Builder in Kotlin Multiplatform SDK" description: "Troubleshoot Paywall Builder in Kotlin Multiplatform SDK" --- This guide helps you resolve common issues when using paywalls designed in the Adapty Paywall Builder in the Kotlin Multiplatform SDK. ## Getting a paywall configuration fails **Issue**: The `createPaywallView` method fails to create a paywall view, or the paywall doesn't have a view configuration. **Reason**: The paywall is not enabled for device display in the Paywall Builder. **Solution**: Enable the **Show on device** toggle in the Paywall Builder. You can also check if a paywall has view configuration by using the `hasViewConfiguration` property on the `AdaptyPaywall` object. ## The paywall view number is too big **Issue**: The paywall view count is showing double the expected number. **Reason**: You may be calling `logShowPaywall` in your code, which duplicates the view count if you're using the Paywall builder. For paywalls designed with the Paywall Builder, analytics are tracked automatically, so you don't need to use this method. **Solution**: Ensure you are not calling `logShowPaywall` in your code if you're using the Paywall builder. --- # File: kmp-implement-paywalls-manually --- --- title: "Implement paywalls manually in Kotlin Multiplatform SDK" description: "Learn how to implement paywalls manually in your Kotlin Multiplatform app with Adapty SDK." --- ## Accept purchases If you are working with paywalls you've implemented yourself, you can delegate handling purchases to Adapty, using the `makePurchase` method. This way, we will handle all the user scenarios, and you will only need to handle the purchase results. :::important `makePurchase` works with products created in the Adapty dashboard. Make sure you configure products and ways to retrieve them in the dashboard by following the [quickstart guide](quickstart). ::: ## Observer mode If you want to implement your own purchase handling logic from scratch, but still want to benefit from the advanced analytics in Adapty, you can use the observer mode. :::important Consider the observer mode limitations [here](observer-vs-full-mode). ::: --- # File: kmp-quickstart-manual --- --- title: "Enable purchases in your custom paywall in Kotlin Multiplatform SDK" description: "Integrate Adapty SDK into your custom Kotlin Multiplatform paywalls to enable in-app purchases." --- This guide describes how to integrate Adapty into your custom paywalls. Keep full control over paywall implementation, while the Adapty SDK fetches products, handles new purchases, and restores previous ones. :::important **This guide is for developers who are implementing custom paywalls.** If you want the easiest way to enable purchases, use the [Adapty Paywall Builder](kmp-quickstart-paywalls.md). With Paywall Builder, you create paywalls in a no-code visual editor, Adapty handles all purchase logic automatically, and you can test different designs without republishing your app. ::: ## Before you start ### Set up products To enable in-app purchases, you need to understand three key concepts: - [**Products**](product.md) – anything users can buy (subscriptions, consumables, lifetime access) - [**Paywalls**](paywalls.md) – configurations that define which products to offer. In Adapty, paywalls are the only way to retrieve products, but this design lets you modify products, prices, and offers without touching your app code. - [**Placements**](placements.md) – where and when you show paywalls in your app (like `main`, `onboarding`, `settings`). You set up paywalls for placements in the dashboard, then request them by placement ID in your code. This makes it easy to run A/B tests and show different paywalls to different users. Make sure you understand these concepts even if you work with your custom paywall. Basically, they are just your way to manage the products you sell in your app. To implement your custom paywall, you will need to create a **paywall** and add it to a **placement**. This setup allows you to retrieve your products. To understand what you need to do in the dashboard, follow the quickstart guide [here](quickstart.md). ### Manage users You can work either with or without backend authentication on your side. However, the Adapty SDK handles anonymous and identified users differently. Read the [identification quickstart guide](kmp-quickstart-identify.md) to understand the specifics and ensure you are working with users properly. ## Step 1. Get products To retrieve products for your custom paywall, you need to: 1. Get the `paywall` object by passing [placement](placements.md) ID to the `getPaywall` method. 2. Get the products array for this paywall using the `getPaywallProducts` method. ```kotlin showLineNumbers fun loadPaywall() { Adapty.getPaywall(placementId = "YOUR_PLACEMENT_ID") .onSuccess { paywall -> Adapty.getPaywallProducts(paywall = paywall) .onSuccess { products -> // Use products to build your custom paywall UI } .onError { error -> // Handle the error } } .onError { error -> // Handle the error } } ``` ## Step 2. Accept purchases When a user taps on a product in your custom paywall, call the `makePurchase` method with the selected product. This will handle the purchase flow and return the updated profile. ```kotlin showLineNumbers fun purchaseProduct(product: AdaptyPaywallProduct) { Adapty.makePurchase(product = product) .onSuccess { purchaseResult -> when (purchaseResult) { is AdaptyPurchaseResult.Success -> { val profile = purchaseResult.profile // Purchase successful, profile updated } is AdaptyPurchaseResult.UserCanceled -> { // User canceled the purchase } is AdaptyPurchaseResult.Pending -> { // Purchase is pending (e.g., user will pay offline with cash) } } } .onError { error -> // Handle the error } } ``` ## Step 3. Restore purchases App stores require all apps with subscriptions to provide a way users can restore their purchases. Call the `restorePurchases` method when the user taps the restore button. This will sync their purchase history with Adapty and return the updated profile. ```kotlin showLineNumbers fun restorePurchases() { Adapty.restorePurchases() .onSuccess { profile -> // Restore successful, profile updated } .onError { error -> // Handle the error } } ``` ## Next steps Your paywall is ready to be displayed in the app. Test your purchases in the [App Store sandbox](test-purchases-in-sandbox) or in [Google Play Store](testing-on-android) to make sure you can complete a test purchase from the paywall. To see how this works in a production-ready implementation, check out the [AppViewModel.kt](https://github.com/adaptyteam/AdaptySDK-KMP/blob/main/example/composeApp/src/commonMain/kotlin/com/adapty/exampleapp/AppViewModel.kt) in our example app, which demonstrates purchase handling with proper error handling and state management. Next, [check whether users have completed their purchase](kmp-check-subscription-status.md) to determine whether to display the paywall or grant access to paid features. --- # File: fetch-paywalls-and-products-kmp --- --- title: "Fetch paywalls and products for remote config paywalls in Kotlin Multiplatform SDK" description: "Fetch paywalls and products in Adapty Kotlin Multiplatform SDK to enhance user monetization." --- Before showcasing remote config and custom paywalls, you need to fetch the information about them. Please be aware that this topic refers to remote config and custom paywalls. For guidance on fetching paywalls for Paywall Builder-customized paywalls, please consult [Fetch Paywall Builder paywalls and their configuration](kmp-get-pb-paywalls). :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. :::
Before you start fetching paywalls and products in your mobile app (click to expand) 1. [Create your products](create-product) in the Adapty Dashboard. 2. [Create a paywall and incorporate the products into your paywall](create-paywall) in the Adapty Dashboard. 3. [Create placements and incorporate your paywall into the placement](create-placement) in the Adapty Dashboard. 4. [Install Adapty SDK](sdk-installation-kotlin-multiplatform.md) in your mobile app.
## Fetch paywall information In Adapty, a [product](product) serves as a combination of products from both the App Store and Google Play. These cross-platform products are integrated into paywalls, enabling you to showcase them within specific mobile app placements. To display the products, you need to obtain a [Paywall](paywalls) from one of your [placements](placements) with `getPaywall` method. :::important **Don't hardcode product IDs.** The only ID you should hardcode is the placement ID. Paywalls are configured remotely, so the number of products and available offers can change at any time. Your app must handle these changes dynamically—if a paywall returns two products today and three tomorrow, display all of them without code changes. ::: ```kotlin showLineNumbers Adapty.getPaywall( placementId = "YOUR_PLACEMENT_ID", locale = "en", fetchPolicy = AdaptyPaywallFetchPolicy.Default, loadTimeout = 5.seconds ).onSuccess { paywall -> // the requested paywall }.onError { error -> // handle the error } ``` | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the [Placement](placements). This is the value you specified when creating a placement in your Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

| | **fetchPolicy** | default: `AdaptyPaywallFetchPolicy.Default` |

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `AdaptyPaywallFetchPolicy.ReturnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

Adapty SDK stores paywalls in two layers: regularly updated cache described above and [fallback paywalls](kmp-use-fallback-paywalls) . We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.

| | **loadTimeout** | default: 5 sec |

This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.

Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.

| Don't hardcode product IDs! Since paywalls are configured remotely, the available products, the number of products, and special offers (such as free trials) can change over time. Make sure your code handles these scenarios. For example, if you initially retrieve 2 products, your app should display those 2 products. However, if you later retrieve 3 products, your app should display all 3 without requiring any code changes. The only thing you have to hardcode is placement ID. Response parameters: | Parameter | Description | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-paywall/) object with: a list of product IDs, the paywall identifier, remote config, and several other properties. | ## Fetch products Once you have the paywall, you can query the product array that corresponds to it: ```kotlin showLineNumbers Adapty.getPaywallProducts(paywall).onSuccess { products -> // the requested products }.onError { error -> // handle the error } ``` Response parameters: | Parameter | Description | | :-------- |:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Products | List of [`AdaptyPaywallProduct`](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-paywall-product/) objects with: product identifier, product name, price, currency, subscription length, and several other properties. | When implementing your own paywall design, you will likely need access to these properties from the [`AdaptyPaywallProduct`](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-paywall-product/) object. Illustrated below are the most commonly used properties, but refer to the linked document for full details on all available properties. | Property | Description | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Title** | To display the title of the product, use `product.localizedTitle`. Note that the localization is based on the users' selected store country rather than the locale of the device itself. | | **Price** | To display a localized version of the price, use `product.price.localizedString`. This localization is based on the locale info of the device. You can also access the price as a number using `product.price.amount`. The value will be provided in the local currency. To get the associated currency symbol, use `product.price.currencySymbol`. | | **Subscription Period** | To display the period (e.g. week, month, year, etc.), use `product.subscriptionDetails?.localizedSubscriptionPeriod`. This localization is based on the locale of the device. To fetch the subscription period programmatically, use `product.subscriptionDetails?.subscriptionPeriod`. From there you can access the `unit` enum to get the length (i.e. DAY, WEEK, MONTH, YEAR, or UNKNOWN). The `numberOfUnits` value will get you the number of period units. For example, for a quarterly subscription, you'd see `MONTH` in the unit property, and `3` in the numberOfUnits property. | | **Introductory Offer** | To display a badge or other indicator that a subscription contains an introductory offer, check out the `product.subscriptionDetails?.introductoryOfferPhases` property. This is a list that can contain up to two discount phases: the free trial phase and the introductory price phase. Within each phase object are the following helpful properties:
• `paymentMode`: an enum with values `FREE_TRIAL`, `PAY_AS_YOU_GO`, `PAY_UPFRONT`, and `UNKNOWN`. Free trials will be the `FREE_TRIAL` type.
• `price`: The discounted price as a number. For free trials, look for `0` here.
• `localizedNumberOfPeriods`: a string localized using the device's locale describing the length of the offer. For example, a three day trial offer shows `3 days` in this field.
• `subscriptionPeriod`: Alternatively, you can get the individual details of the offer period with this property. It works in the same manner for offers as the previous section describes.
• `localizedSubscriptionPeriod`: A formatted subscription period of the discount for the user's locale. | ## Speed up paywall fetching with default audience paywall Typically, paywalls are fetched almost instantly, so you don't need to worry about speeding up this process. However, in cases where you have numerous audiences and paywalls, and your users have a weak internet connection, fetching a paywall may take longer than you'd like. In such situations, you might want to display a default paywall to ensure a smooth user experience rather than showing no paywall at all. To address this, you can use the `getPaywallForDefaultAudience` method, which fetches the paywall of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the paywall by the `getPaywall` method, as detailed in the [Fetch Paywall Information](fetch-paywalls-and-products-kmp#fetch-paywall-information) section above. :::warning Why we recommend using `getPaywall` The `getPaywallForDefaultAudience` method comes with a few significant drawbacks: - **Potential backward compatibility issues**: If you need to show different paywalls for different app versions (current and future), you may face challenges. You'll either have to design paywalls that support the current (legacy) version or accept that users with the current (legacy) version might encounter issues with non-rendered paywalls. - **Loss of targeting**: All users will see the same paywall designed for the **All Users** audience, which means you lose personalized targeting (including based on countries, marketing attribution or your own custom attributes). If you're willing to accept these drawbacks to benefit from faster paywall fetching, use the `getPaywallForDefaultAudience` method as follows. Otherwise, stick to the `getPaywall` described [above](fetch-paywalls-and-products-kmp#fetch-paywall-information). ::: ```kotlin showLineNumbers Adapty.getPaywallForDefaultAudience( placementId = "YOUR_PLACEMENT_ID", locale = "en", fetchPolicy = AdaptyPaywallFetchPolicy.Default ).onSuccess { paywall -> // the requested paywall }.onError { error -> // handle the error } ``` | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the [Placement](placements). This is the value you specified when creating a placement in your Adapty Dashboard. | | **locale** |

optional

default: `en`

|

The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

| | **fetchPolicy** | default: `AdaptyPaywallFetchPolicy.Default` |

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `AdaptyPaywallFetchPolicy.ReturnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

| --- # File: present-remote-config-paywalls-kmp --- --- title: "Render paywall designed by remote config in Kotlin Multiplatform SDK" description: "Discover how to present remote config paywalls in Adapty Kotlin Multiplatform SDK to personalize user experience." --- If you've customized a paywall using remote config, you'll need to implement rendering in your mobile app's code to display it to users. Since remote config offers flexibility tailored to your needs, you're in control of what's included and how your paywall view appears. We provide a method for fetching the remote configuration, giving you the autonomy to showcase your custom paywall configured via remote config. ## Get paywall remote config and present it To get a remote config of a paywall, access the `remoteConfig` property and extract the needed values. ```kotlin showLineNumbers Adapty.getPaywall( placementId = "YOUR_PLACEMENT_ID", locale = "en", fetchPolicy = AdaptyPaywallFetchPolicy.Default, loadTimeout = 5.seconds ).onSuccess { paywall -> val headerText = paywall.remoteConfig?.dataMap?.get("header_text") as? String // use the remote config values }.onError { error -> // handle the error } ``` At this point, once you've received all the necessary values, it's time to render and assemble them into a visually appealing page. Ensure that the design accommodates various mobile phone screens and orientations, providing a seamless and user-friendly experience across different devices. :::warning Make sure to [record the paywall view event](present-remote-config-paywalls-kmp#track-paywall-view-events) as described below, allowing Adapty analytics to capture information for funnels and A/B tests. ::: After you've done with displaying the paywall, continue with setting up a purchase flow. When the user makes a purchase, simply call `.makePurchase()` with the product from your paywall. For details on the`.makePurchase()` method, read [Making purchases](kmp-making-purchases). We recommend [creating a backup paywall called a fallback paywall](kmp-use-fallback-paywalls). This backup will display to the user when there's no internet connection or cache available, ensuring a smooth experience even in these situations. ## Track paywall view events Adapty assists you in measuring the performance of your paywalls. While we gather data on purchases automatically, logging paywall views needs your input because only you know when a customer sees a paywall. To log a paywall view event, simply call `.logShowPaywall(paywall)`, and it will be reflected in your paywall metrics in funnels and A/B tests. :::important Calling `.logShowPaywall(paywall)` is not needed if you are displaying paywalls created in the [paywall builder](adapty-paywall-builder.md). ::: ```kotlin showLineNumbers Adapty.logShowPaywall(paywall = paywall) .onSuccess { // paywall view logged successfully } .onError { error -> // handle the error } ``` Request parameters: | Parameter | Presence | Description | | :---------- | :------- |:-------------------------------------------------------------------------------------------------------| | **paywall** | required | An [`AdaptyPaywall`](https://kmp.adapty.io//////adapty/com.adapty.kmp.models/-adapty-paywall/) object. | --- # File: kmp-making-purchases --- --- title: "Make purchases in mobile app in Kotlin Multiplatform SDK" description: "Guide on handling in-app purchases and subscriptions using Adapty." --- Displaying paywalls within your mobile app is an essential step in offering users access to premium content or services. However, simply presenting these paywalls is enough to support purchases only if you use [Paywall Builder](adapty-paywall-builder) to customize your paywalls. If you don't use the Paywall Builder, you must use a separate method called `.makePurchase()` to complete a purchase and unlock the desired content. This method serves as the gateway for users to engage with the paywalls and proceed with their desired transactions. If your paywall has an active promotional offer for the product a user is trying to buy, Adapty will automatically apply it at the time of purchase. :::warning Keep in mind that the introductory offer will be applied automatically only if you use the paywalls set up using the Paywall Builder. In other cases, you'll need to [verify the user's eligibility for an introductory offer on iOS](fetch-paywalls-and-products-kmp#check-intro-offer-eligibility-on-ios). Skipping this step may result in your app being rejected during release. Moreover, it could lead to charging the full price to users who are eligible for an introductory offer. ::: Make sure you've [done the initial configuration](quickstart) without skipping a single step. Without it, we can't validate purchases. ## Make purchase :::note **Using [Paywall Builder](adapty-paywall-builder)?** Purchases are processed automatically—you can skip this step. **Looking for step-by-step guidance?** Check out the [quickstart guide](kmp-implement-paywalls-manually) for end-to-end implementation instructions with full context. ::: ```kotlin showLineNumbers Adapty.makePurchase(product = product).onSuccess { purchaseResult -> when (purchaseResult) { is AdaptyPurchaseResult.Success -> { val profile = purchaseResult.profile if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive == true) { // Grant access to the paid features } } is AdaptyPurchaseResult.UserCanceled -> { // Handle the case where the user canceled the purchase } is AdaptyPurchaseResult.Pending -> { // Handle deferred purchases (e.g., the user will pay offline with cash) } } }.onError { error -> // Handle the error } ``` Request parameters: | Parameter | Presence | Description | | :---------- | :------- |:----------------------------------------------------------------------------------------------------------------------------------------------| | **Product** | required | An [`AdaptyPaywallProduct`](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-paywall-product/) object retrieved from the paywall. | Response parameters: | Parameter | Description | |---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Profile** |

If the request has been successful, the response contains this object. An [AdaptyProfile](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-profile/) object provides comprehensive information about a user's access levels, subscriptions, and non-subscription purchases within the app.

Check the access level status to ascertain whether the user has the required access to the app.

| :::warning **Note:** if you're still on Apple's StoreKit version lower than v2.0 and Adapty SDK version lowers than v.2.9.0, you need to provide [Apple App Store shared secret](app-store-connection-configuration#step-4-enter-app-store-shared-secret) instead. This method is currently deprecated by Apple. ::: ## Change subscription when making a purchase When a user opts for a new subscription instead of renewing the current one, the way it works depends on the app store. For Google Play, the subscription isn't automatically updated. You'll need to manage the switch in your mobile app code as described below. To replace the subscription with another one in Android, call `.makePurchase()` method with the additional parameter: ```kotlin showLineNumbers val subscriptionUpdateParams = AdaptyAndroidSubscriptionUpdateParameters( oldSubVendorProductId = "old_subscription_product_id", replacementMode = AdaptyAndroidSubscriptionUpdateReplacementMode.CHARGE_FULL_PRICE ) val purchaseParams = AdaptyPurchaseParameters.Builder() .setSubscriptionUpdateParams(subscriptionUpdateParams) .build() Adapty.makePurchase( product = product, parameters = purchaseParams ).onSuccess { purchaseResult -> when (purchaseResult) { is AdaptyPurchaseResult.Success -> { val profile = purchaseResult.profile // successful cross-grade } is AdaptyPurchaseResult.UserCanceled -> { // user canceled the purchase flow } is AdaptyPurchaseResult.Pending -> { // the purchase has not been finished yet, e.g. user will pay offline by cash } } }.onError { error -> // Handle the error } ``` Additional request parameter: | Parameter | Presence | Description | |:---------------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **parameters** | optional | an [`AdaptyAndroidSubscriptionUpdateParameters`](https://kmp.adapty.io/////adapty/com.adapty.kmp.models/-adapty-android-subscription-update-parameters/) object passed through [`AdaptyPurchaseParameters`](https://kmp.adapty.io/adapty/com.adapty.kmp.models/-adapty-purchase-parameters/). | You can read more about subscriptions and replacement modes in the Google Developer documentation: - [About replacement modes](https://developer.android.com/google/play/billing/subscriptions#replacement-modes) - [Recommendations from Google for replacement modes](https://developer.android.com/google/play/billing/subscriptions#replacement-recommendations) - Replacement mode [`CHARGE_PRORATED_PRICE`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#CHARGE_PRORATED_PRICE()). Note: this method is available only for subscription upgrades. Downgrades are not supported. - Replacement mode [`DEFERRED`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#DEFERRED()). Note: A real subscription change will occur only when the current subscription billing period ends. ## Redeem Offer Code in iOS Since iOS 14.0, your users can redeem Offer Codes. Code redemption means using a special code, like a promotional or gift card code, to get free access to content or features in an app or on the App Store. To enable users to redeem offer codes, you can display the offer code redemption sheet by using the appropriate SDK method: ```kotlin showLineNumbers Adapty.presentCodeRedemptionSheet() .onSuccess { // code redemption sheet presented successfully } .onError { error -> // handle the error } ``` :::danger Based on our observations, the Offer Code Redemption sheet in some apps may not work reliably. We recommend redirecting the user directly to the App Store. In order to do this, you need to open the url of the following format: `https://apps.apple.com/redeem?ctx=offercodes&id={apple_app_id}&code={code}` ::: ## Manage prepaid plans (Android) If your app users can purchase [prepaid plans](https://developer.android.com/google/play/billing/subscriptions#prepaid-plans) (e.g., buy a non-renewable subscription for several months), you can enable [pending transactions](https://developer.android.com/google/play/billing/subscriptions#pending) for prepaid plans. ```kotlin showLineNumbers Adapty.activate( AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withGoogleEnablePendingPrepaidPlans(true) .build() ).onSuccess { // successful activation }.onError { error -> // handle the error } ``` --- # File: kmp-restore-purchase --- --- title: "Restore purchases in mobile app in Kotlin Multiplatform SDK" description: "Learn how to restore purchases in Adapty to ensure seamless user experience." --- Restoring Purchases is a feature that allows users to regain access to previously purchased content, such as subscriptions or in-app purchases, without being charged again. This feature is especially useful for users who may have uninstalled and reinstalled the app or switched to a new device and want to access their previously purchased content without paying again. :::note In paywalls built with [Paywall Builder](adapty-paywall-builder), purchases are restored automatically without additional code from you. If that's your case — you can skip this step. ::: To restore a purchase if you do not use the [Paywall Builder](adapty-paywall-builder) to customize the paywall, call `.restorePurchases()` method: ```kotlin showLineNumbers Adapty.restorePurchases().onSuccess { profile -> if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive == true) { // successful access restore } }.onError { error -> // handle the error } ``` Response parameters: | Parameter | Description | |---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Profile** |

An [`AdaptyProfile`](https://kmp.adapty.io//////adapty/com.adapty.kmp.models/-adapty-profile/) object. This model contains info about access levels, subscriptions, and non-subscription purchases.

Сheck the **access level status** to determine whether the user has access to the app.

| :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: --- # File: implement-observer-mode-kmp --- --- title: "Implement Observer mode in Kotlin Multiplatform SDK" description: "Implement observer mode in Adapty to track user subscription events in Kotlin Multiplatform SDK." --- If you already have your own purchase infrastructure and aren't ready to fully switch to Adapty, you can explore [Observer mode](observer-vs-full-mode). In its basic form, Observer Mode offers advanced analytics and seamless integration with attribution and analytics systems. If this meets your needs, you only need to: 1. Turn it on when configuring the Adapty SDK by setting the `observerMode` parameter to `true`. Follow the setup instructions for [Kotlin Multiplatform](sdk-installation-kotlin-multiplatform.md). 2. [Report transactions](report-transactions-observer-mode-kmp) from your existing purchase infrastructure to Adapty. ## Observer mode setup Turn on the Observer mode if you handle purchases and subscription status yourself and use Adapty for sending subscription events and analytics. :::important When running in Observer mode, Adapty SDK won't close any transactions, so make sure you're handling it. ::: ```kotlin showLineNumbers val config = AdaptyConfig .Builder("PUBLIC_SDK_KEY") .withObserverMode(true) // default false .build() Adapty.activate(configuration = config) .onSuccess { Log.d("Adapty", "SDK initialised in observer mode") } .onError { error -> Log.e("Adapty", "Adapty init error: ${error.message}") } ``` Parameters: | Parameter | Description | | --------------------------- | ------------------------------------------------------------ | | observerMode | A boolean value that controls [Observer mode](observer-vs-full-mode). The default value is `false`. | ## Using Adapty paywalls in Observer Mode If you also want to use Adapty's paywalls and A/B testing features, you can — but it requires some extra setup in Observer mode. Here's what you'll need to do in addition to the steps above: 1. Display paywalls as usual for [remote config paywalls](present-remote-config-paywalls-kmp.md). 3. [Associate paywalls](report-transactions-observer-mode-kmp) with purchase transactions. --- # File: report-transactions-observer-mode-kmp --- --- title: "Report transactions in Observer Mode in Kotlin Multiplatform SDK" description: "Report purchase transactions in Adapty Observer Mode for user insights and revenue tracking in Kotlin Multiplatform SDK." --- In Observer mode, the Adapty SDK can't track purchases made through your existing purchase system on its own. You need to report transactions from your app store. It's crucial to set this up **before** releasing your app to avoid errors in analytics. Use `reportTransaction` to explicitly report each transaction for Adapty to recognize it. :::warning **Don't skip transaction reporting!** If you don't call `reportTransaction`, Adapty won't recognize the transaction, it won't appear in analytics, and it won't be sent to integrations. ::: If you use Adapty paywalls, include the `variationId` when reporting a transaction. This links the purchase to the paywall that triggered it, ensuring accurate paywall analytics. ```kotlin showLineNumbers Adapty.reportTransaction( transactionId = "your_transaction_id", variationId = paywall.variationId ).onSuccess { profile -> // Transaction reported successfully // profile contains updated user data }.onError { error -> // handle the error } ``` Parameters: | Parameter | Presence | Description | | --------------- | -------- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | transactionId | required | The transaction ID from your app store purchase. This is typically the purchase token or transaction identifier returned by the store. | | variationId | optional | The string identifier of the variation. You can get it using `variationId` property of the [AdaptyPaywall](https://kmp.adapty.io//////adapty/com.adapty.kmp.models/-adapty-paywall/) object. | --- # File: kmp-troubleshoot-purchases --- --- title: "Troubleshoot purchases in Kotlin Multiplatform SDK" description: "Troubleshoot purchases in Kotlin Multiplatform SDK" --- This guide helps you resolve common issues when implementing purchases manually in the Kotlin Multiplatform SDK. ## makePurchase is called successfully, but the profile is not being updated **Issue**: The `makePurchase` method completes successfully, but the user's profile and subscription status are not updated in Adapty. **Reason**: This usually indicates incomplete Google Play Store setup or configuration issues. **Solution**: Ensure you've completed all the [Google Play setup steps](https://adapty.io/docs/initial-android). ## makePurchase is invoked twice **Issue**: The `makePurchase` method is being called multiple times for the same purchase. **Reason**: This typically happens when the purchase flow is triggered multiple times due to UI state management issues or rapid user interactions. **Solution**: Ensure you've completed all the [Google Play setup steps](https://adapty.io/docs/initial-android). ## AdaptyError.cantMakePayments in observer mode **Issue**: You're getting `AdaptyError.cantMakePayments` when using `makePurchase` in observer mode. **Reason**: In observer mode, you should handle purchases on your side, not use Adapty's `makePurchase` method. **Solution**: If you use `makePurchase` for purchases, turn off the observer mode. You need either to use `makePurchase` or handle purchases on your side in the observer mode. See [Implement Observer mode](implement-observer-mode-kmp) for more details. ## Adapty error: (code: 103, message: Play Market request failed on purchases updated: responseCode=3, debugMessage=Billing Unavailable, detail: null) **Issue**: You're receiving a billing unavailable error from Google Play Store. **Reason**: This error is not related to Adapty. It's a Google Play Billing Library error indicating that billing is not available on the device. **Solution**: This error is not related to Adapty. You can check and find out more about it in the Play Store documentation: [Handle BillingResult response codes](https://developer.android.com/google/play/billing/errors#billing_unavailable_error_code_3) | Play Billing | Android Developers. ## Not found makePurchasesCompletionHandlers **Issue**: You're encountering issues with `makePurchasesCompletionHandlers` not being found. **Reason**: This is typically related to sandbox testing issues. **Solution**: Create a new sandbox user and try again. This often resolves sandbox-related purchase completion handler issues. --- # File: kmp-user --- --- title: "Users & access in Kotlin Multiplatform SDK" description: "Learn how to work with users and access levels in your Kotlin Multiplatform app with Adapty SDK." --- This page contains all guides for working with users and access levels in your Kotlin Multiplatform app. Choose the topic you need: - **[Identify users](kmp-identifying-users)** - Learn how to identify users in your app - **[Update user data](kmp-setting-user-attributes)** - Set user attributes and profile data - **[Listen for subscription status changes](kmp-listen-subscription-changes)** - Monitor subscription changes in real-time - **[Kids Mode](kids-mode-kmp)** - Implement Kids Mode for your app --- # File: kmp-identifying-users --- --- title: "Identify users in Kotlin Multiplatform SDK" description: "Identify users in Adapty to improve personalized subscription experiences." --- Adapty creates an internal profile ID for every user. However, if you have your own authentication system, you should set your own Customer User ID. You can find users by their Customer User ID in the [Profiles](profiles-crm) section and use it in the [server-side API](getting-started-with-server-side-api), which will be sent to all integrations. ### Setting customer user ID on configuration If you have a user ID during configuration, just pass it as `customerUserId` parameter to `.activate()` method: ```kotlin showLineNumbers Adapty.activate( AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withCustomerUserId("YOUR_USER_ID") .build() ).onSuccess { // successful activation }.onError { error -> // handle the error } } ``` :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ### Setting customer user ID after configuration If you don't have a user ID in the SDK configuration, you can set it later at any time with the `.identify()` method. The most common cases for using this method are after registration or authorization, when the user switches from being an anonymous user to an authenticated user. ```kotlin showLineNumbers Adapty.identify("YOUR_USER_ID").onSuccess { // successful identify }.onError { error -> // handle the error } ``` Request parameters: - **Customer User ID** (required): a string user identifier. :::warning Resubmitting of significant user data In some cases, such as when a user logs into their account again, Adapty's servers already have information about that user. In these scenarios, the Adapty SDK will automatically switch to work with the new user. If you passed any data to the anonymous user, such as custom attributes or attributions from third-party networks, you should resubmit that data for the identified user. It's also important to note that you should re-request all paywalls and products after identifying the user, as the new user's data may be different. ::: ### Logging out and logging in You can log the user out anytime by calling `.logout()` method: ```kotlin showLineNumbers Adapty.logout().onSuccess { // successful logout }.onError { error -> // handle the error } ``` You can then login the user using `.identify()` method. ## Assign `appAccountToken` (iOS) [`iosAppAccountToken`](https://developer.apple.com/documentation/storekit/product/purchaseoption/appaccounttoken(_:)) is a **UUID** that lets you link App Store transactions to your internal user identity. StoreKit associates this token with every transaction, so your backend can match App Store data to your users. Use a stable UUID generated per user and reuse it for the same account across devices. This ensures that purchases and App Store notifications stay correctly linked. You can set the token in two ways – during the SDK activation or when identifying the user. :::important You must always pass `iosAppAccountToken` together with `customerUserId`. If you pass only the token, it will not be included in the transaction. ::: ```kotlin showLineNumbers // During configuration: Adapty.activate( AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withCustomerUserId( id = "YOUR_USER_ID", iosAppAccountToken = "YOUR_IOS_APP_ACCOUNT_TOKEN" ) .build() ).onSuccess { // successful activation }.onError { error -> // handle the error } // Or when identifying users Adapty.identify( customerUserId = "YOUR_USER_ID", iosAppAccountToken = "YOUR_IOS_APP_ACCOUNT_TOKEN" ).onSuccess { // successful identify }.onError { error -> // handle the error } ``` ## Set obfuscated account IDs (Android) Google Play requires obfuscated account IDs for certain use cases to enhance user privacy and security. These IDs help Google Play identify purchases while keeping user information anonymous, which is particularly important for fraud prevention and analytics. You may need to set these IDs if your app handles sensitive user data or if you're required to comply with specific privacy regulations. The obfuscated IDs allow Google Play to track purchases without exposing actual user identifiers. :::important You must always pass `androidObfuscatedAccountId` together with `customerUserId`. If you pass only the obfuscated account ID, it will not be included in the transaction. ::: ```kotlin showLineNumbers // During configuration: Adapty.activate( AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withCustomerUserId( id = "YOUR_USER_ID", androidObfuscatedAccountId = "YOUR_OBFUSCATED_ACCOUNT_ID" ) .build() ).onSuccess { // successful activation }.onError { error -> // handle the error } // Or when identifying users Adapty.identify( customerUserId = "YOUR_USER_ID", androidObfuscatedAccountId = "YOUR_OBFUSCATED_ACCOUNT_ID" ).onSuccess { // successful identify }.onError { error -> // handle the error } ``` --- # File: kmp-setting-user-attributes --- --- title: "Set user attributes in Kotlin Multiplatform SDK" description: "Learn how to set user attributes in Adapty to enable better audience segmentation." --- You can set optional attributes such as email, phone number, etc, to the user of your app. You can then use attributes to create user [segments](segments) or just view them in CRM. ### Setting user attributes To set user attributes, call `.updateProfile()` method: ```kotlin showLineNumbers val builder = AdaptyProfileParameters.Builder() .withEmail("email@email.com") .withPhoneNumber("+18888888888") .withFirstName("John") .withLastName("Appleseed") .withGender(AdaptyProfile.Gender.FEMALE) .withBirthday(AdaptyProfile.Date(1970, 1, 3)) Adapty.updateProfile(builder.build()) .onSuccess { // profile updated successfully } .onError { error -> // handle the error } ``` Please note that the attributes that you've previously set with the `updateProfile` method won't be reset. :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ### The allowed keys list The allowed keys `` of `AdaptyProfileParameters.Builder` and the values `` are listed below: | Key | Value | |---|-----| |

email

phoneNumber

firstName

lastName

| String | | gender | Enum, allowed values are: `AdaptyProfile.Gender.FEMALE`, `AdaptyProfile.Gender.MALE`, `AdaptyProfile.Gender.OTHER` | | birthday | Date | ### Custom user attributes You can set your own custom attributes. These are usually related to your app usage. For example, for fitness applications, they might be the number of exercises per week, for language learning app user's knowledge level, and so on. You can use them in segments to create targeted paywalls and offers, and you can also use them in analytics to figure out which product metrics affect the revenue most. ```kotlin showLineNumbers val builder = AdaptyProfileParameters.Builder() builder.withCustomAttribute("key1", "value1") ``` To remove existing key, use `.withRemovedCustomAttribute()` method: ```kotlin showLineNumbers val builder = AdaptyProfileParameters.Builder() builder.withRemovedCustomAttribute("key2") ``` Sometimes you need to figure out what custom attributes have already been installed before. To do this, use the `customAttributes` field of the `AdaptyProfile` object. :::warning Keep in mind that the value of `customAttributes` may be out of date since the user attributes can be sent from different devices at any time so the attributes on the server might have been changed after the last sync. ::: ### Limits - Up to 30 custom attributes per user - Key names are up to 30 characters long. The key name can include alphanumeric characters and any of the following: `_` `-` `.` - Value can be a string or float with no more than 50 characters. --- # File: kmp-listen-subscription-changes --- --- title: "Check subscription status in Kotlin Multiplatform SDK" description: "Track and manage user subscription status in Adapty for improved customer retention in your Kotlin Multiplatform app." --- With Adapty, keeping track of subscription status is made easy. You don't have to manually insert product IDs into your code. Instead, you can effortlessly confirm a user's subscription status by checking for an active [access level](access-level). Before you start checking subscription status, set up [Real-time Developer Notifications (RTDN)](enable-real-time-developer-notifications-rtdn). ## Access level and the AdaptyProfile object Access levels are properties of the [AdaptyProfile](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-profile/) object. We recommend retrieving the profile when your app starts, such as when you [identify a user](android-identifying-users#setting-customer-user-id-on-configuration) , and then updating it whenever changes occur. This way, you can use the profile object without repeatedly requesting it. To be notified of profile updates, listen for profile changes as described in the [Listening for profile updates, including access levels](android-listen-subscription-changes.md) section below. :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ## Retrieving the access level from the server To get the access level from the server, use the `.getProfile()` method: ```kotlin showLineNumbers Adapty.getProfile().onSuccess { profile -> // check the access }.onError { error -> // handle the error } ``` Response parameters: | Parameter | Description | | --------- |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Profile |

An [AdaptyProfile](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-profile/) object. Generally, you have to check only the access level status of the profile to determine whether the user has premium access to the app.

The `.getProfile` method provides the most up-to-date result as it always tries to query the API. If for some reason (e.g. no internet connection), the Adapty SDK fails to retrieve information from the server, the data from the cache will be returned. It is also important to note that the Adapty SDK updates `AdaptyProfile` cache regularly, to keep this information as up-to-date as possible.

| The `.getProfile()` method provides you with the user profile from which you can get the access level status. You can have multiple access levels per app. For example, if you have a newspaper app and sell subscriptions to different topics independently, you can create access levels "sports" and "science". But most of the time, you will only need one access level, in that case, you can just use the default "premium" access level. Here is an example for checking for the default "premium" access level: ```kotlin showLineNumbers Adapty.getProfile().onSuccess { profile -> if (profile.accessLevels["premium"]?.isActive == true) { // grant access to premium features } }.onError { error -> // handle the error } ``` ### Listening for subscription status updates Whenever the user's subscription changes, Adapty fires an event. To receive messages from Adapty, you need to make some additional configuration: ```kotlin showLineNumbers Adapty.setOnProfileUpdatedListener { profile -> // handle any changes to subscription state } ``` Adapty also fires an event at the start of the application. In this case, the cached subscription status will be passed. ### Subscription status cache The cache implemented in the Adapty SDK stores the subscription status of the profile. This means that even if the server is unavailable, the cached data can be accessed to provide information about the profile's subscription status. However, it's important to note that direct data requests from the cache are not possible. The SDK periodically queries the server every minute to check for any updates or changes related to the profile. If there are any modifications, such as new transactions or other updates, they will be sent to the cached data in order to keep it synchronized with the server. --- # File: kmp-deal-with-att --- --- title: "Deal with ATT in Kotlin Multiplatform SDK" description: "Get started with Adapty on Kotlin Multiplatform to streamline subscription setup and management." --- If your application uses AppTrackingTransparency framework and presents an app-tracking authorization request to the user, then you should send the [authorization status](https://developer.apple.com/documentation/apptrackingtransparency/attrackingmanager/authorizationstatus/) to Adapty. ```kotlin showLineNumbers val profileParameters = AdaptyProfileParameters.Builder() .withAttStatus(3) // 3 = ATTrackingManagerAuthorizationStatusAuthorized .build() Adapty.updateProfile(profileParameters) .onSuccess { // ATT status updated successfully } .onError { error -> // handle AdaptyError } ``` :::warning We strongly recommend that you send this value as early as possible when it changes, only in that case the data will be sent in a timely manner to the integrations you have configured. ::: --- # File: kids-mode-kmp --- --- title: "Kids Mode in Kotlin Multiplatform SDK" description: "Easily enable Kids Mode to comply with Google policies. No GAID or ad data collected in Kotlin Multiplatform SDK." --- If your Kotlin Multiplatform application is intended for kids, you must follow the policies of [Google](https://support.google.com/googleplay/android-developer/answer/9893335). If you're using the Adapty SDK, a few simple steps will help you configure it to meet these policies and pass app store reviews. ## What's required? You need to configure the Adapty SDK to disable the collection of: - [IDFA (Identifier for Advertisers)](https://en.wikipedia.org/wiki/Identifier_for_Advertisers) (iOS) - [Android Advertising ID (AAID/GAID)](https://support.google.com/googleplay/android-developer/answer/6048248) (Android) - [IP address](https://www.ftc.gov/system/files/ftc_gov/pdf/p235402_coppa_application.pdf) In addition, we recommend using customer user ID carefully. User ID in format `` will be definitely treated as gathering personal data as well as using email. For Kids Mode, a best practice is to use randomized or anonymized identifiers (e.g., hashed IDs or device-generated UUIDs) to ensure compliance. ## Enabling Kids Mode ### Updates in the Adapty Dashboard In the Adapty Dashboard, you need to disable the IP address collection. To do this, go to [App settings](https://app.adapty.io/settings/general) and click **Disable IP address collection** under **Collect users' IP address**. ### Updates in your mobile app code To comply with policies, you need to disable the collection of the Android Advertising ID (AAID/GAID) and IP address when initializing the Adapty SDK: ```kotlin showLineNumbers override fun onCreate() { super.onCreate() val config = AdaptyConfig .Builder("PUBLIC_SDK_KEY") // highlight-start .withGoogleAdvertisingIdCollectionDisabled(true) // set to `true` .withIpAddressCollectionDisabled(true) // set to `true` // highlight-end .build() Adapty.activate(configuration = config) .onSuccess { Log.d("Adapty", "SDK initialised with privacy settings") } .onError { error -> Log.e("Adapty", "Adapty init error: ${error.message}") } } ``` --- # File: kmp-onboardings --- --- title: "Onboardings in Kotlin Multiplatform SDK" description: "Learn how to work with onboardings in your Kotlin Multiplatform app with Adapty SDK." --- --- # File: kmp-get-onboardings --- --- title: "Get onboardings in Kotlin Multiplatform SDK" description: "Learn how to retrieve onboardings in Adapty for Kotlin Multiplatform." --- After [you designed the visual part for your onboarding](design-onboarding.md) with the builder in the Adapty Dashboard, you can display it in your Kotlin Multiplatform app. The first step in this process is to get the onboarding associated with the placement and its view configuration as described below. Before you start, ensure that: 1. You have installed [Adapty Kotlin Multiplatform SDK](sdk-installation-kotlin-multiplatform.md) version 3.15.0 or higher. 2. You have [created an onboarding](create-onboarding.md). 3. You have added the onboarding to a [placement](placements.md). ## Fetch onboarding When you create an [onboarding](onboardings.md) with our no-code builder, it's stored as a container with configuration that your app needs to fetch and display. This container manages the entire experience - what content appears, how it's presented, and how user interactions (like quiz answers or form inputs) are processed. The container also automatically tracks analytics events, so you don't need to implement separate view tracking. For the best performance, fetch the onboarding configuration early to give images enough time to download before showing to users. To get an onboarding, use the `getOnboarding` method: ```kotlin showLineNumbers Adapty.getOnboarding( placementId = "YOUR_PLACEMENT_ID", locale = "en", fetchPolicy = AdaptyPaywallFetchPolicy.Default, loadTimeout = 5.seconds ).onSuccess { paywall -> // the requested paywall }.onError { error -> // handle the error } ``` Parameters: | Parameter | Presence | Description | |---------|--------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. | | **locale** |

optional

default: `en`

| The identifier of the onboarding localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.

Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.

| | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

Adapty SDK stores onboardings locally in two layers: regularly updated cache described above and fallback onboardings. We also use CDN to fetch onboardings faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your onboardings while ensuring reliability even in cases where internet connection is scarce.

| | **loadTimeout** | default: 5 sec |

This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.

Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.

| Response parameters: | Parameter | Description | |:----------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Onboarding | An [`AdaptyOnboarding`](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-onboarding/) object with: the onboarding identifier and configuration, remote config, and several other properties. | ## Speed up onboarding fetching with default audience onboarding Typically, onboardings are fetched almost instantly, so you don't need to worry about speeding up this process. However, in cases where you have numerous audiences and onboardings, and your users have a weak internet connection, fetching a onboarding may take longer than you'd like. In such situations, you might want to display a default onboarding to ensure a smooth user experience rather than showing no onboarding at all. To address this, you can use the `getOnboardingForDefaultAudience` method, which fetches the onboarding of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the onboarding by the `getOnboarding` method, as detailed in the [Fetch Onboarding](#fetch-onboarding) section above. :::warning Consider using `getOnboarding` instead of `getOnboardingForDefaultAudience`, as the latter has important limitations: - **Compatibility issues**: May create problems when supporting multiple app versions, requiring either backward-compatible designs or accepting that older versions might display incorrectly. - **No personalization**: Only shows content for the "All Users" audience, removing targeting based on country, attribution, or custom attributes. If faster fetching outweighs these drawbacks for your use case, use `getOnboardingForDefaultAudience` as shown below. Otherwise, use `getOnboarding` as described [above](#fetch-onboarding). ::: ```kotlin showLineNumbers Adapty.getOnboardingForDefaultAudience( placementId = "YOUR_PLACEMENT_ID", locale = "en", fetchPolicy = AdaptyPaywallFetchPolicy.Default, ).onSuccess { paywall -> // the requested paywall }.onError { error -> // handle the error } ``` Parameters: | Parameter | Presence | Description | |---------|--------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. | | **locale** |

optional

default: `en`

| The identifier of the onboarding localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language. | | **fetchPolicy** | default: `.reloadRevalidatingCacheData` |

By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.

However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.

Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.

Adapty SDK stores onboardings locally in two layers: regularly updated cache described above and fallback onboardings. We also use CDN to fetch onboardings faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your onboardings while ensuring reliability even in cases where internet connection is scarce.

| --- # File: kmp-present-onboardings --- --- title: "Present onboardings in Kotlin Multiplatform SDK" description: "Learn how to present onboardings effectively to drive more conversions." --- If you've customized an onboarding using the builder, you don't need to worry about rendering it in your Kotlin Multiplatform app code to display it to the user. Such an onboarding contains both what should be shown within the onboarding and how it should be shown. Before you start, ensure that: 1. You have installed [Adapty Kotlin Multiplatform SDK](sdk-installation-kotlin-multiplatform.md) 3.15.0 or later. 2. You have [created an onboarding](create-onboarding.md). 3. You have added the onboarding to a [placement](placements.md). To display an onboarding, use the `view.present()` method on the `view` created by the `createOnboardingView` method. Each `view` can only be used once. If you need to display the onboarding again, call `createPaywallView` one more to create a new `view` instance. :::warning Reusing the same `view` without recreating it may result in an error. ::: ```kotlin showLineNumbers title="Kotlin Multiplatform" viewModelScope.launch { AdaptyUI.createOnboardingView(onboarding = onboarding).onSuccess { view -> view.present() }.onError { error -> // handle the error } } ``` ## Configure iOS presentation style Configure how the onboarding is presented on iOS by passing the `iosPresentationStyle` parameter to the `present()` method. The parameter accepts `AdaptyUIIOSPresentationStyle.FULLSCREEN` (default) or `AdaptyUIIOSPresentationStyle.PAGESHEET` values. ```kotlin showLineNumbers viewModelScope.launch { val view = AdaptyUI.createOnboardingView(onboarding = onboarding).getOrNull() view?.present(iosPresentationStyle = AdaptyUIIOSPresentationStyle.PAGESHEET) } ``` ## Customize how links open in onboardings By default, links in onboardings open in an in-app browser. This provides a seamless user experience by displaying web pages within your application, allowing users to view them without switching apps. If you prefer to open links in an external browser instead, you can customize this behavior by setting the `externalUrlsPresentation` parameter to `AdaptyWebPresentation.EXTERNAL_BROWSER`: ```kotlin showLineNumbers viewModelScope.launch { AdaptyUI.createOnboardingView( onboarding = onboarding, externalUrlsPresentation = AdaptyWebPresentation.EXTERNAL_BROWSER // default – IN_APP_BROWSER ).onSuccess { view -> view.present() }.onError { error -> // handle the error } } ``` --- # File: kmp-handling-onboarding-events --- --- title: "Handle onboarding events in Kotlin Multiplatform SDK" description: "Handle onboarding-related events in Kotlin Multiplatform using Adapty." --- Before you start, ensure that: 1. You have installed [Adapty Kotlin Multiplatform SDK](sdk-installation-kotlin-multiplatform.md) 3.15.0 or later. 2. You have [created an onboarding](create-onboarding.md). 3. You have added the onboarding to a [placement](placements.md). Onboardings configured with the builder generate events your app can respond to. Learn how to respond to these events below. ## Set up the onboarding event observer To handle onboarding events, you need to implement the `AdaptyUIOnboardingsEventsObserver` interface and set it up with `AdaptyUI.setOnboardingsEventsObserver()`. This should be done early in your app's lifecycle, typically in your main activity or app initialization. ```kotlin // In your app initialization AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver()) ``` ## Custom actions In the builder, you can add a **custom** action to a button and assign it an ID. Then, you can use this ID in your code and handle it as a custom action. For example, if a user taps a custom button, like **Login** or **Allow notifications**, the delegate method `onCustomAction` will be triggered with the action ID from the builder. You can create your own IDs, like "allowNotifications". ```kotlin class MyAdaptyUIOnboardingsEventsObserver : AdaptyUIOnboardingsEventsObserver { override fun onboardingViewOnCustomAction( view: AdaptyUIOnboardingView, meta: AdaptyUIOnboardingMeta, actionId: String ) { when (actionId) { "openPaywall" -> { // Display paywall from onboarding // You would typically fetch and present a new paywall here mainUiScope.launch { // Example: Get paywall by placement ID // val paywallResult = Adapty.getPaywall("your_placement_id") // paywallResult.onSuccess { paywall -> // val paywallViewResult = AdaptyUI.createPaywallView(paywall) // paywallViewResult.onSuccess { paywallView -> // paywallView.present() // } // } } } "allowNotifications" -> { // Handle notification permissions } else -> { // Handle other custom actions } } } } // Set up the observer AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver()) ```
Event example (Click to expand) ```json { "actionId": "allowNotifications", "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "screensTotal": 3 } } ```
## Closing onboarding Onboarding is considered closed when a user taps a button with the **Close** action assigned. You need to manage what happens when a user closes the onboarding. For example: :::important You need to manage what happens when a user closes the onboarding. For instance, you need to stop displaying the onboarding itself. ::: ```kotlin class MyAdaptyUIOnboardingsEventsObserver : AdaptyUIOnboardingsEventsObserver { override fun onboardingViewOnCloseAction( view: AdaptyUIOnboardingView, meta: AdaptyUIOnboardingMeta, actionId: String ) { // Dismiss the onboarding screen mainUiScope.launch { view.dismiss() } // Additional cleanup or navigation logic can be added here // For example, navigate back or show main app content } } // Set up the observer AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver()) ```
Event example (Click to expand) ```json { "action_id": "close_button", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "final_screen", "screen_index": 3, "total_screens": 4 } } ```
## Opening a paywall :::tip Handle this event to open a paywall if you want to open it inside the onboarding. If you want to open a paywall after it is closed, there is a more straightforward way to do it – handle [`onboardingViewOnCloseAction`](#closing-onboarding) and open a paywall without relying on the event data. ::: If a user clicks a button that opens a paywall, you will get a button action ID that you [set up manually](get-paid-in-onboardings.md). The most seamless way to work with paywalls in onboardings is to make the action ID equal to a paywall placement ID. This way, you can use the placement ID to get and open the paywall right away: ```kotlin class MyAdaptyUIOnboardingsEventsObserver : AdaptyUIOnboardingsEventsObserver { override fun onboardingViewOnPaywallAction( view: AdaptyUIOnboardingView, meta: AdaptyUIOnboardingMeta, actionId: String ) { // Get the paywall using the placement ID from the action mainUiScope.launch { val paywallResult = Adapty.getPaywall(placementId = actionId) paywallResult.onSuccess { paywall -> val paywallViewResult = AdaptyUI.createPaywallView(paywall) paywallViewResult.onSuccess { paywallView -> paywallView.present() }.onError { error -> // handle the error } }.onError { error -> // handle the error } } } } // Set up the observer AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver()) ```
Event example (Click to expand) ```json { "action_id": "premium_offer_1", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "pricing_screen", "screen_index": 2, "total_screens": 4 } } ```
## Finishing loading onboarding When an onboarding finishes loading, this method will be invoked: ```kotlin class MyAdaptyUIOnboardingsEventsObserver : AdaptyUIOnboardingsEventsObserver { override fun onboardingViewDidFinishLoading( view: AdaptyUIOnboardingView, meta: AdaptyUIOnboardingMeta ) { // Handle loading completion // You can add any initialization logic here } } // Set up the observer AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver()) ```
Event example (Click to expand) ```json { "meta": { "onboarding_id": "onboarding_123", "screen_cid": "welcome_screen", "screen_index": 0, "total_screens": 4 } } ```
## Navigation events The `onboardingViewOnAnalyticsEvent` method is called when various analytics events occur during the onboarding flow. The `event` object can be one of the following types: |Type | Description | |------------|-------------| | `AdaptyOnboardingsAnalyticsEventOnboardingStarted` | When the onboarding has been loaded | | `AdaptyOnboardingsAnalyticsEventScreenPresented` | When any screen is shown | | `AdaptyOnboardingsAnalyticsEventScreenCompleted` | When a screen is completed. Includes optional `elementId` (identifier of the completed element) and optional `reply` (response from the user). Triggered when users perform any action to exit the screen. | | `AdaptyOnboardingsAnalyticsEventSecondScreenPresented` | When the second screen is shown | | `AdaptyOnboardingsAnalyticsEventUserEmailCollected` | Triggered when the user's email is collected via the input field | | `AdaptyOnboardingsAnalyticsEventOnboardingCompleted` | Triggered when a user reaches a screen with the `final` ID. If you need this event, assign the `final` ID to the last screen. | | `AdaptyOnboardingsAnalyticsEventUnknown` | For any unrecognized event type. Includes `name` (the name of the unknown event) and `meta` (additional metadata) | Each event includes `meta` information containing: | Field | Description | |------------|-------------| | `onboardingId` | Unique identifier of the onboarding flow | | `screenClientId` | Identifier of the current screen | | `screenIndex` | Current screen's position in the flow | | `screensTotal` | Total number of screens in the flow | Here's an example of how you can use analytics events for tracking: ```kotlin class MyAdaptyUIOnboardingsEventsObserver : AdaptyUIOnboardingsEventsObserver { override fun onboardingViewOnAnalyticsEvent( view: AdaptyUIOnboardingView, meta: AdaptyUIOnboardingMeta, event: AdaptyOnboardingsAnalyticsEvent ) { when (event) { is AdaptyOnboardingsAnalyticsEventOnboardingStarted -> { // Track onboarding start trackEvent("onboarding_started", event.meta) } is AdaptyOnboardingsAnalyticsEventScreenPresented -> { // Track screen presentation trackEvent("screen_presented", event.meta) } is AdaptyOnboardingsAnalyticsEventScreenCompleted -> { // Track screen completion with user response trackEvent("screen_completed", event.meta, event.elementId, event.reply) } is AdaptyOnboardingsAnalyticsEventOnboardingCompleted -> { // Track successful onboarding completion trackEvent("onboarding_completed", event.meta) } is AdaptyOnboardingsAnalyticsEventUnknown -> { // Handle unknown events trackEvent(event.name, event.meta) } // Handle other cases as needed } } private fun trackEvent(eventName: String, meta: AdaptyUIOnboardingMeta, elementId: String? = null, reply: String? = null) { // Implement your analytics tracking here // For example, send to your analytics service } } // Set up the observer AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver()) ```
Event examples (Click to expand) ```javascript // OnboardingStarted { "meta": { "onboardingId": "onboarding_123", "screenClientId": "welcome_screen", "screenIndex": 0, "screensTotal": 4 } } // ScreenPresented { "meta": { "onboardingId": "onboarding_123", "screenClientId": "interests_screen", "screenIndex": 2, "screensTotal": 4 } } // ScreenCompleted { "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 1, "screensTotal": 4 }, "elementId": "profile_form", "reply": "success" } // SecondScreenPresented { "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 1, "screensTotal": 4 } } // UserEmailCollected { "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 1, "screensTotal": 4 } } // OnboardingCompleted { "meta": { "onboardingId": "onboarding_123", "screenClientId": "final_screen", "screenIndex": 3, "screensTotal": 4 } } ```
--- # File: kmp-onboarding-input --- --- title: "Process data from onboardings in Kotlin Multiplatform SDK" description: "Save and use data from onboardings in your Kotlin Multiplatform app with Adapty SDK." --- When your users respond to a quiz question or input their data into an input field, the `onboardingViewOnStateUpdatedAction` method will be invoked. You can save or process the field type in your code. For example: ```kotlin class MyAdaptyUIOnboardingsEventsObserver : AdaptyUIOnboardingsEventsObserver { override fun onboardingViewOnStateUpdatedAction( view: AdaptyUIOnboardingView, meta: AdaptyUIOnboardingMeta, elementId: String, params: AdaptyOnboardingsStateUpdatedParams ) { // Store user preferences or responses when (params) { is AdaptyOnboardingsSelectParams -> { // Handle single selection val id = params.id val value = params.value val label = params.label AppLogger.d("Selected option: $label (id: $id, value: $value)") } is AdaptyOnboardingsMultiSelectParams -> { // Handle multiple selections } is AdaptyOnboardingsInputParams -> { // Handle text input } is AdaptyOnboardingsDatePickerParams -> { // Handle date selection } } } } // Set up the observer AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver()) ```
Saved data examples (the format may differ in your implementation) ```javascript // Example of a saved select action { "id": "onboarding_on_state_updated_action", "view": { /* AdaptyUI.OnboardingView object */ }, "meta": { "onboarding_id": "onboarding_123", "screen_cid": "preferences_screen", "screen_index": 1, "total_screens": 3 }, "action": { "element_id": "preference_selector", "element_type": "select", "value": { "id": "option_1", "value": "premium", "label": "Premium Plan" } } } // Example of a saved multi-select action { "id": "onboarding_on_state_updated_action", "view": { /* AdaptyUI.OnboardingView object */ }, "meta": { "onboarding_id": "onboarding_123", "screen_cid": "interests_screen", "screen_index": 2, "total_screens": 3 }, "action": { "element_id": "interests_selector", "element_type": "multi_select", "value": [ { "id": "interest_1", "value": "sports", "label": "Sports" }, { "id": "interest_2", "value": "music", "label": "Music" } ] } } // Example of a saved input action { "id": "onboarding_on_state_updated_action", "view": { /* AdaptyUI.OnboardingView object */ }, "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 0, "total_screens": 3 }, "action": { "element_id": "name_input", "element_type": "input", "value": { "type": "text", "value": "John Doe" } } } // Example of a saved date picker action { "id": "onboarding_on_state_updated_action", "view": { /* AdaptyUI.OnboardingView object */ }, "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 0, "total_screens": 3 }, "action": { "element_id": "birthday_picker", "element_type": "date_picker", "value": { "day": 15, "month": 6, "year": 1990 } } } ```
## Use cases ### Enrich user profiles with data If you want to immediately link the input data with the user profile and avoid asking them twice for the same info, you need to [update the user profile](kmp-setting-user-attributes.md) with the input data when handling the action. For example, you ask users to enter their name in the text field with the `name` ID, and you want to set this field's value as user's first name. Also, you ask them to enter their email in the `email` field. In your app code, it can look like this: ```kotlin class MyAdaptyUIOnboardingsEventsObserver : AdaptyUIOnboardingsEventsObserver { override fun onboardingViewOnStateUpdatedAction( view: AdaptyUIOnboardingView, meta: AdaptyUIOnboardingMeta, elementId: String, params: AdaptyOnboardingsStateUpdatedParams ) { // Store user preferences or responses when (params) { is AdaptyOnboardingsInputParams -> { // Handle text input val builder = AdaptyProfileParameters.Builder() // Map elementId to appropriate profile field when (elementId) { "name" -> { when (val input = params.input) { is AdaptyOnboardingsTextInput -> { builder.withFirstName(input.value) } } } "email" -> { when (val input = params.input) { is AdaptyOnboardingsEmailInput -> { builder.withEmail(input.value) } } } } // Update profile asynchronously mainUiScope.launch { val profileParams = builder.build() val result = Adapty.updateProfile(profileParams) result.onSuccess { profile -> // Profile updated successfully AppLogger.d("Profile updated: ${profile.email}") }.onError { error -> // Handle the error AppLogger.e("Failed to update profile: ${error.message}") } } } } } } // Set up the observer AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver()) ``` ### Customize paywalls based on answers Using quizzes in onboardings, you can also customize paywalls you show users after they complete the onboarding. For example, you can ask users about their experience with sport and show different CTAs and products to different user groups. 1. [Add a quiz](onboarding-quizzes.md) in the onboarding builder and assign meaningful IDs to its options. 2. Handle the quiz responses based on their IDs and [set custom attributes](kmp-setting-user-attributes.md) for users. ```kotlin class MyAdaptyUIOnboardingsEventsObserver : AdaptyUIOnboardingsEventsObserver { override fun onboardingViewOnStateUpdatedAction( view: AdaptyUIOnboardingView, meta: AdaptyUIOnboardingMeta, elementId: String, params: AdaptyOnboardingsStateUpdatedParams ) { // Handle quiz responses and set custom attributes when (params) { is AdaptyOnboardingsSelectParams -> { // Handle quiz selection val builder = AdaptyProfileParameters.Builder() // Map quiz responses to custom attributes when (elementId) { "experience" -> { // Set custom attribute 'experience' with the selected value (beginner, amateur, pro) builder.withCustomAttribute("experience", params.value) } } // Update profile asynchronously mainUiScope.launch { val profileParams = builder.build() val result = Adapty.updateProfile(profileParams) result.onSuccess { profile -> // Profile updated successfully AppLogger.d("Custom attribute 'experience' set to: ${params.value}") }.onError { error -> // Handle the error AppLogger.e("Failed to update profile: ${error.message}") } } } } } } // Set up the observer AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver()) ``` 3. [Create segments](segments.md) for each custom attribute value. 4. Create a [placement](placements.md) and add [audiences](audience.md) for each segment you've created. 5. [Display a paywall](kmp-paywalls.md) for the placement in your app code. If your onboarding has a button that opens a paywall, implement the paywall code as a [response to this button's action](kmp-handling-onboarding-events.md#opening-a-paywall). --- # File: kmp-test --- --- title: "Test & release in Kotlin Multiplatform SDK" description: "Learn how to check subscription status in your Kotlin Multiplatform app with Adapty." --- If you've already implemented the Adapty SDK in your Kotlin Multiplatform app, you'll want to test that everything is set up correctly and that purchases work as expected. This involves testing both the SDK integration and the actual purchase flow with the sandbox environment. ## Test your app For comprehensive testing of your in-app purchases, see our platform-specific testing guides: [iOS testing guide](test-purchases-in-sandbox.md) and [Android testing guide](testing-on-android.md). ## Prepare for release Before submitting your app to the store, follow the [Release checklist](release-checklist) to confirm: - Store connection and server notifications are configured - Purchases complete and are reported to Adapty - Access unlocks and restores correctly - Privacy and review requirements are met --- # File: kmp-reference --- --- title: "Reference for Kotlin Multiplatform SDK" description: "Reference documentation for Adapty Kotlin Multiplatform SDK." --- This page contains reference documentation for Adapty Kotlin Multiplatform SDK. Choose the topic you need: - **[SDK models](https://kmp.adapty.io/adapty/)** - Data models and structures used by the SDK - **[Handle errors](kmp-handle-errors)** - Error handling and troubleshooting --- # File: kmp-handle-errors --- --- title: "Handle errors in Kotlin Multiplatform SDK" description: "Learn how to handle errors in your Kotlin Multiplatform app with Adapty." --- This page covers error handling in the Adapty Kotlin Multiplatform SDK. ## Error handling basics All Adapty SDK methods return results that can be either success or error. Always handle both cases: ```kotlin showLineNumbers Adapty.getProfile { result -> when (result) { is AdaptyResult.Success -> { val profile = result.value // Handle success } is AdaptyResult.Error -> { val error = result.error // Handle error Log.e("Adapty", "Error: ${error.message}") } } } ``` ```java showLineNumbers Adapty.getProfile(result -> { if (result instanceof AdaptyResult.Success) { AdaptyProfile profile = ((AdaptyResult.Success) result).getValue(); // Handle success } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // Handle error Log.e("Adapty", "Error: " + error.getMessage()); } }); ``` ## Common error codes | Error Code | Description | Solution | |------------|-------------|----------| | 1000 | No product IDs found | Check product configuration in dashboard | | 1001 | Network error | Check internet connection | | 1002 | Invalid SDK key | Verify your SDK key | | 1003 | Can't make payments | Device doesn't support payments | | 1004 | Product not available | Product not configured in store | ## Handle specific errors ### Network errors ```kotlin showLineNumbers Adapty.getPaywall("main") { result -> when (result) { is AdaptyResult.Success -> { val paywall = result.value // Use paywall } is AdaptyResult.Error -> { val error = result.error when (error.code) { 1001 -> { // Network error - show offline message showOfflineMessage() } else -> { // Other errors showErrorMessage(error.message) } } } } } ``` ```java showLineNumbers Adapty.getPaywall("main", result -> { if (result instanceof AdaptyResult.Success) { AdaptyPaywall paywall = ((AdaptyResult.Success) result).getValue(); // Use paywall } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); switch (error.getCode()) { case 1001: // Network error - show offline message showOfflineMessage(); break; default: // Other errors showErrorMessage(error.getMessage()); break; } } }); ``` ### Purchase errors ```kotlin showLineNumbers product.makePurchase { result -> when (result) { is AdaptyResult.Success -> { val purchase = result.value // Purchase successful showSuccessMessage() } is AdaptyResult.Error -> { val error = result.error when (error.code) { 1003 -> { // Can't make payments showPaymentNotAvailableMessage() } 1004 -> { // Product not available showProductNotAvailableMessage() } else -> { // Other purchase errors showPurchaseErrorMessage(error.message) } } } } } ``` ```java showLineNumbers product.makePurchase(result -> { if (result instanceof AdaptyResult.Success) { AdaptyPurchase purchase = ((AdaptyResult.Success) result).getValue(); // Purchase successful showSuccessMessage(); } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); switch (error.getCode()) { case 1003: // Can't make payments showPaymentNotAvailableMessage(); break; case 1004: // Product not available showProductNotAvailableMessage(); break; default: // Other purchase errors showPurchaseErrorMessage(error.getMessage()); break; } } }); ``` ## Error recovery strategies ### Retry on network errors ```kotlin showLineNumbers fun getPaywallWithRetry(placementId: String, maxRetries: Int = 3) { var retryCount = 0 fun attemptGetPaywall() { Adapty.getPaywall(placementId) { result -> when (result) { is AdaptyResult.Success -> { val paywall = result.value // Use paywall } is AdaptyResult.Error -> { val error = result.error if (error.code == 1001 && retryCount < maxRetries) { // Network error - retry retryCount++ Handler(Looper.getMainLooper()).postDelayed({ attemptGetPaywall() }, 1000 * retryCount) // Exponential backoff } else { // Max retries reached or other error showErrorMessage(error.message) } } } } } attemptGetPaywall() } ``` ```java showLineNumbers public void getPaywallWithRetry(String placementId, int maxRetries) { AtomicInteger retryCount = new AtomicInteger(0); Runnable attemptGetPaywall = new Runnable() { @Override public void run() { Adapty.getPaywall(placementId, result -> { if (result instanceof AdaptyResult.Success) { AdaptyPaywall paywall = ((AdaptyResult.Success) result).getValue(); // Use paywall } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); if (error.getCode() == 1001 && retryCount.get() < maxRetries) { // Network error - retry retryCount.incrementAndGet(); new Handler(Looper.getMainLooper()).postDelayed(this, 1000 * retryCount.get()); } else { // Max retries reached or other error showErrorMessage(error.getMessage()); } } }); } }; attemptGetPaywall.run(); } ``` ### Fallback to cached data ```kotlin showLineNumbers class PaywallManager { private var cachedPaywall: AdaptyPaywall? = null fun getPaywall(placementId: String) { Adapty.getPaywall(placementId) { result -> when (result) { is AdaptyResult.Success -> { val paywall = result.value cachedPaywall = paywall showPaywall(paywall) } is AdaptyResult.Error -> { val error = result.error if (error.code == 1001 && cachedPaywall != null) { // Network error - use cached paywall showPaywall(cachedPaywall!!) showOfflineIndicator() } else { // No cache available or other error showErrorMessage(error.message) } } } } } } ``` ```java showLineNumbers public class PaywallManager { private AdaptyPaywall cachedPaywall; public void getPaywall(String placementId) { Adapty.getPaywall(placementId, result -> { if (result instanceof AdaptyResult.Success) { AdaptyPaywall paywall = ((AdaptyResult.Success) result).getValue(); cachedPaywall = paywall; showPaywall(paywall); } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); if (error.getCode() == 1001 && cachedPaywall != null) { // Network error - use cached paywall showPaywall(cachedPaywall); showOfflineIndicator(); } else { // No cache available or other error showErrorMessage(error.getMessage()); } } }); } } ``` ## Next steps - [Fix for Code-1000 noProductIDsFound error](InvalidProductIdentifiers-kmp.md) - [Fix for Code-1003 cantMakePayments error](cantMakePayments-kmp.md) - [Complete API reference](https://android.adapty.io) - Full SDK documentation --- # File: InvalidProductIdentifiers-kmp --- --- title: "Fix for Code-1000 noProductIDsFound error in Kotlin Multiplatform SDK" description: "Resolve invalid product identifier errors when managing subscriptions in Adapty." --- The 1000-code error, `noProductIDsFound`, indicates that none of the products you requested on the paywall are available for purchase in the App Store, even though they're listed there. This error may sometimes come with an `InvalidProductIdentifiers` warning. If the warning appears without an error, safely ignore it. If you're encountering the `noProductIDsFound` error, follow these steps to resolve it: ## Step 1. Check bundle ID \{#step-2-check-bundle-id\} --- no_index: true --- 1. Open [App Store Connect](https://appstoreconnect.apple.com/apps). Select your app and proceed to **General** → **App Information** section. 2. Copy the **Bundle ID** in the **General Information** sub-section. 3. Open the [**App settings** -> **iOS SDK** tab](https://app.adapty.io/settings/ios-sdk) from the Adapty top menu and paste the copied value to the **Bundle ID** field. 4. Get back to the **App information** page in App Store Connect and copy **Apple ID** from there. 5. On the [**App settings** -> **iOS SDK**](https://app.adapty.io/settings/ios-sdk) page in the Adapty dashboard, paste the ID to the **Apple app ID** field. ## Step 2. Check products \{#step-3-check-products\} 1. Go to **App Store Connect** and navigate to [**Monetization** → **Subscriptions**](https://appstoreconnect.apple.com/apps/6477523342/distribution/subscriptions) in the left-hand menu. 2. Click on the subscription group name. You'll see your products listed under the **Subscriptions** section. 3. Ensure the product you're testing is marked **Ready to Submit**. If not, follow the instructions on the [Product in App Store](app-store-products) page. 4. Compare the product ID from the table with the one in the [**Products**](https://app.adapty.io/products) tab in the Adapty Dashboard. If the IDs don't match, copy the product ID from the table and [create a product](create-product) with it in the Adapty Dashboard. ## Step 3. Check product availability \{#step-4-check-product-availability\} 1. Go back to **App Store Connect** and open the same **Subscriptions** section. 2. Click the subscription group name to view your products. 3. Select the product you're testing. 4. Scroll to the **Availability** section and check that all the required countries and regions are listed. ## Step 4. Check product prices \{#step-5-check-product-prices\} 1. Again, head to the **Monetization** → **Subscriptions** section in **App Store Connect**. 2. Click the subscription group name. 3. Select the product you're testing. 4. Scroll down to **Subscription Pricing** and expand the **Current Pricing for New Subscribers** section. 5. Ensure that all required prices are listed. ## Step 5. Check app paid status, bank account, and tax forms are active 1. In **App Store Connect**](https://appstoreconnect.apple.com/) homepage, click **Business**. 2. Select your company name. 3. Scroll down and check that your **Paid Apps Agreement**, **Bank Account**, and **Tax forms** all show as **Active**. By following these steps, you should be able to resolve the `InvalidProductIdentifiers` warning and get your products live in the store --- # File: cantMakePayments-kmp --- --- title: "Fix for Code-1003 cantMakePayment error in Kotlin Multiplatform SDK" description: "Resolve making payments error when managing subscriptions in Adapty." --- The 1003 error, `cantMakePayments`, indicates that in-app purchases can't be made on this device. If you’re encountering the `cantMakePayments` error, this is usually due to one of the reasons: - Device restrictions: The error is not related to Adapty. See the ways to fix the issue below. - Observer mode configuration: The `makePurchase` method and the observer mode can't be used at the same time. See the section below. ## Issue: Device restrictions | Issue | Solution | |---------------------------|---------------------------------------------------------| | Screen Time restrictions | Disable In-App Purchase restrictions in [Screen Time](https://support.apple.com/en-us/102470) | | Account suspended | Contact Apple Support to resolve account issues | | Regional restrictions | Use App Store account from supported region | ## Issue: Using both Observer mode and makePurchase If you are using `makePurchases` to handle purchases, you don't need to use Observer mode. [Observer mode](https://adapty.io/docs/observer-vs-full-mode) is only needed if you implement the purchase logic yourself. So, if you're using `makePurchase`, you can safely remove enabling Observer mode from the SDK activation code. --- # File: kmp-sdk-migration-guides --- --- title: "Kotlin Multiplatform SDK Migration Guides" description: "Migration guides for Adapty Kotlin Multiplatform SDK versions." --- This page contains all migration guides for Adapty Kotlin Multiplatform SDK. Choose the version you want to migrate to for detailed instructions: - **[Migrate to v. 3.15](migration-to-kmp-315)** --- # File: migration-to-kmp-315 --- --- title: "Migration guide to Adapty Kotlin Multiplatform SDK 3.15.0" description: "Migration steps for Adapty Kotlin Multiplatform SDK 3.15.0" --- Adapty Kotlin Multiplatform SDK 3.15.0 is a major release that brings new features and improvements which, however, may require some migration steps from you. 1. Update the observer class and method names. 2. Update the fallback paywalls method name. 3. Update the view class name in event handling methods. ## Update observer class and method names The observer class and its registration method have been renamed: ```diff - import com.adapty.kmp.AdaptyUIObserver + import com.adapty.kmp.AdaptyUIPaywallsEventsObserver - import com.adapty.kmp.models.AdaptyUIView + import com.adapty.kmp.models.AdaptyUIPaywallView - class MyAdaptyUIObserver : AdaptyUIObserver { - override fun paywallViewDidPerformAction(view: AdaptyUIView, action: AdaptyUIAction) { + class MyAdaptyUIPaywallsEventsObserver : AdaptyUIPaywallsEventsObserver { + override fun paywallViewDidPerformAction(view: AdaptyUIPaywallView, action: AdaptyUIAction) { // handle actions } } // Set up the observer - AdaptyUI.setObserver(MyAdaptyUIObserver()) + AdaptyUI.setPaywallsEventsObserver(MyAdaptyUIPaywallsEventsObserver()) ``` ## Update fallback paywalls method name The method name for setting fallback paywalls has been changed: ```diff showLineNumbers - Adapty.setFallbackPaywalls(assetId = "fallback.json") + Adapty.setFallback(assetId = "fallback.json") .onSuccess { // Fallback paywalls loaded successfully } .onError { error -> // Handle the error } ``` ## Update view class name in event handling methods All event handling methods now use the new `AdaptyUIPaywallView` class instead of `AdaptyUIView`: ```diff - override fun paywallViewDidAppear(view: AdaptyUIView) { + override fun paywallViewDidAppear(view: AdaptyUIPaywallView) { // Handle paywall appearance } - override fun paywallViewDidDisappear(view: AdaptyUIView) { + override fun paywallViewDidDisappear(view: AdaptyUIPaywallView) { // Handle paywall disappearance } - override fun paywallViewDidSelectProduct(view: AdaptyUIPaywallView, productId: String) { + override fun paywallViewDidSelectProduct(view: AdaptyUIView, productId: String) { // Handle product selection } - override fun paywallViewDidStartPurchase(view: AdaptyUIView, product: AdaptyPaywallProduct) { + override fun paywallViewDidStartPurchase(view: AdaptyUIPaywallView, product: AdaptyPaywallProduct) { // Handle purchase start } - override fun paywallViewDidFinishPurchase(view: AdaptyUIView, product: AdaptyPaywallProduct, purchaseResult: AdaptyPurchaseResult) { + override fun paywallViewDidFinishPurchase(view: AdaptyUIPaywallView, product: AdaptyPaywallProduct, purchaseResult: AdaptyPurchaseResult) { // Handle purchase result } - override fun paywallViewDidFailPurchase(view: AdaptyUIView, product: AdaptyPaywallProduct, error: AdaptyError) { + override fun paywallViewDidFailPurchase(view: AdaptyUIPaywallView, product: AdaptyPaywallProduct, error: AdaptyError) { // Add your purchase failure handling logic here } - override fun paywallViewDidFinishRestore(view: AdaptyUIView, profile: AdaptyProfile) { + override fun paywallViewDidFinishRestore(view: AdaptyUIPaywallView, profile: AdaptyProfile) { // Add your successful restore handling logic here } - override fun paywallViewDidFailRestore(view: AdaptyUIView, error: AdaptyError) { + override fun paywallViewDidFailRestore(view: AdaptyUIPaywallView, error: AdaptyError) { // Add your restore failure handling logic here } - override fun paywallViewDidFinishWebPaymentNavigation(view: AdaptyUIView, product: AdaptyPaywallProduct?, error: AdaptyError?) { + override fun paywallViewDidFinishWebPaymentNavigation(view: AdaptyUIPaywallView, product: AdaptyPaywallProduct?, error: AdaptyError?) { // Handle web payment navigation result } - override fun paywallViewDidFailLoadingProducts(view: AdaptyUIView, error: AdaptyError) { + override fun paywallViewDidFailLoadingProducts(view: AdaptyUIPaywallView, error: AdaptyError) { // Add your product loading failure handling logic here } - override fun paywallViewDidFailRendering(view: AdaptyUIView, error: AdaptyError) { + override fun paywallViewDidFailRendering(view: AdaptyUIPaywallView, error: AdaptyError) { // Handle rendering error } ``` --- # End of Documentation _Generated on: 2026-03-05T16:27:48.193Z_ _Successfully processed: 41/41 files_ # REACT-NATIVE - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Generated on: 2026-03-05T16:27:48.194Z Total files: 42 --- # File: sdk-installation-react-native-expo --- --- title: "Install & configure Adapty React Native SDK in an Expo project" description: "Step-by-step guide on installing Adapty React Native SDK in an Expo project for subscription-based apps." --- :::important This guide covers installing and configuring the Adapty React Native SDK **in an Expo project**. If you’re using **pure React Native (without Expo)**, follow the [React Native installation guide](sdk-installation-react-native-pure.md) instead. ::: Adapty SDK includes two key modules for seamless integration into your React Native app: - **Core Adapty**: This module is required for Adapty to function properly in your app. - **AdaptyUI**: This module is needed if you use the [Adapty Paywall Builder](adapty-paywall-builder), a user-friendly, no-code tool for easily creating cross-platform paywalls. AdaptyUI is automatically activated along with the core module. If you need a full tutorial on how to implement IAP in your React Native app, check [this](https://adapty.io/blog/react-native-in-app-purchases-tutorial/) out. :::tip Want to see a real-world example of how Adapty SDK is integrated into an Expo app? Check out our sample apps: - [Expo dev build sample](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples/FocusJournalExpo) for full functionality including real purchases and Paywall Builder - [Expo Go & Web sample](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples/ExpoGoWebMock) for testing with mock mode ::: For a complete implementation walkthrough, you can also see the video:
## Requirements The Adapty React Native SDK supports iOS 13.0+, but using paywalls created in the [Adapty paywall builder](adapty-paywall-builder.md) requires iOS 15.0+. :::info Adapty is compatible with Google Play Billing Library up to 8.x. By default, Adapty works with Google Play Billing Library v.7.0.0 but, if you want to force a later version, you can manually [add the dependency](https://developer.android.com/google/play/billing/integrate#dependency). For Expo, this can be done during prebuild or via a config plugin. ::: ## Install Adapty SDK [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-React-Native.svg?style=flat&logo=react)](https://github.com/adaptyteam/AdaptySDK-React-Native/releases) :::important [Expo Dev Client](https://docs.expo.dev/versions/latest/sdk/dev-client/) (a custom development build) is required to use Adapty in an Expo project. Expo Go doesn't support custom native modules, so you can only use it with [**mock mode**](#set-up-mock-mode-for-expo-go--expo-web) for UI/logic development (no real purchases and no AdaptyUI/Paywall Builder rendering). ::: 1. Install Adapty SDK (this also installs `@adapty/core` automatically): ```sh npx expo install react-native-adapty npx expo prebuild ``` 2. Build your app for development using EAS or local build: ```sh # For iOS eas build --profile development --platform ios # For Android eas build --profile development --platform android ``` ```sh # For iOS npx expo run:ios # For Android npx expo run:android ``` 3. Start the dev server: ```sh npx expo start --dev-client ``` ## Activate Adapty module of Adapty SDK To get your **Public SDK Key**: 1. Go to Adapty Dashboard and navigate to [**App settings → General**](https://app.adapty.io/settings/general). 2. From the **Api keys** section, copy the **Public SDK Key** (NOT the Secret Key). 3. Replace `"YOUR_PUBLIC_SDK_KEY"` in the code. :::important - Make sure you use the **Public SDK key** for Adapty initialization, the **Secret key** should be used for [server-side API](getting-started-with-server-side-api) only. - **SDK keys** are unique for every app, so if you have multiple apps make sure you choose the right one. ::: Copy the following code to `App.tsx` to activate Adapty: ```typescript showLineNumbers title="App.tsx" adapty.activate('YOUR_PUBLIC_SDK_KEY'); ``` :::tip To avoid activation errors in the development environment, use the [tips](#development-environment-tips). ::: Now set up paywalls in your app: - If you use [Adapty Paywall Builder](adapty-paywall-builder), follow the [Paywall Builder quickstart](react-native-quickstart-paywalls). - If you build your own paywall UI, see the [quickstart for custom paywalls](react-native-quickstart-manual). ## Activate AdaptyUI module of Adapty SDK If you plan to use [Paywall Builder](adapty-paywall-builder.md), you need the AdaptyUI module. It is activated automatically when you activate the core module; you don't need to do anything else. ## Optional setup ### Logging #### Set up the logging system Adapty logs errors and other important information to help you understand what is going on. There are the following levels available: | Level | Description | | ---------- | ------------------------------------------------------------ | | `error` | Only errors will be logged | | `warn` | Errors and messages from the SDK that do not cause critical errors, but are worth paying attention to will be logged | | `info` | Errors, warnings, and various information messages will be logged | | `verbose` | Any additional information that may be useful during debugging, such as function calls, API queries, etc. will be logged | You can set the log level in your app before or during Adapty configuration: ```typescript showLineNumbers title="App.tsx" // Set log level before activation // 'verbose' is recommended for development and the first production release adapty.setLogLevel('verbose'); // Or set it during configuration adapty.activate('YOUR_PUBLIC_SDK_KEY', { logLevel: 'verbose', }); ``` ### Data policies Adapty doesn't store personal data of your users unless you explicitly send it, but you can implement additional data security policies to comply with the store or country guidelines. #### Disable IP address collection and sharing When activating the Adapty module, set `ipAddressCollectionDisabled` to `true` to disable user IP address collection and sharing. The default value is `false`. Use this parameter to enhance user privacy, comply with regional data protection regulations (like GDPR or CCPA), or reduce unnecessary data collection when IP-based features aren't required for your app. ```typescript showLineNumbers title="App.tsx" adapty.activate('YOUR_PUBLIC_SDK_KEY', { ipAddressCollectionDisabled: true, }); ``` #### Disable advertising ID collection and sharing When activating the Adapty module, set `ios.idfaCollectionDisabled` (iOS) or `android.adIdCollectionDisabled` (Android) to `true` to disable the collection of advertising identifiers. The default value is `false`. Use this parameter to comply with App Store/Play Store policies, avoid triggering the App Tracking Transparency prompt, or if your app does not require advertising attribution or analytics based on advertising IDs. ```typescript showLineNumbers title="App.tsx" adapty.activate('YOUR_PUBLIC_SDK_KEY', { ios: { idfaCollectionDisabled: true, }, android: { adIdCollectionDisabled: true, }, }); ``` #### Set up media cache configuration for AdaptyUI By default, AdaptyUI caches media (such as images and videos) to improve performance and reduce network usage. You can customize the cache settings by providing a custom configuration. Use `mediaCache` to override the default cache settings: ```typescript adapty.activate('YOUR_PUBLIC_SDK_KEY', { mediaCache: { memoryStorageTotalCostLimit: 200 * 1024 * 1024, // Optional: memory cache size in bytes memoryStorageCountLimit: 2147483647, // Optional: max number of items in memory diskStorageSizeLimit: 200 * 1024 * 1024, // Optional: disk cache size in bytes }, }); ``` Parameters: | Parameter | Required | Description | |-----------|----------|-------------| | memoryStorageTotalCostLimit | optional | Total cache size in memory in bytes. Defaults to platform-specific value. | | memoryStorageCountLimit | optional | The item count limit of the memory storage. Defaults to platform-specific value. | | diskStorageSizeLimit | optional | The file size limit on disk in bytes. Defaults to platform-specific value. | ### Enable local access levels (Android) By default, [local access levels](local-access-levels.md) are enabled on iOS and disabled on Android. To enable them on Android as well, set `localAccessLevelAllowed` to `true`: ```typescript showLineNumbers title="App.tsx" adapty.activate('YOUR_PUBLIC_SDK_KEY', { android: { localAccessLevelAllowed: true, }, }); ``` ### Clear data on backup restore When `clearDataOnBackup` is set to `true`, the SDK detects when the app is restored from an iCloud backup and deletes all locally stored SDK data, including cached profile information, product details, and paywalls. The SDK then initializes with a clean state. Default value is `false`. :::note Only local SDK cache is deleted. Transaction history with Apple and user data on Adapty servers remain unchanged. ::: ```typescript showLineNumbers title="App.tsx" adapty.activate('YOUR_PUBLIC_SDK_KEY', { ios: { clearDataOnBackup: true }, }); ``` ## Development environment tips #### Set up mock mode for Expo Go / Expo Web Expo Go and Expo Web environments don't have access to Adapty's native modules. To avoid runtime errors while still being able to build and test your app's UI and paywall logic, Adapty provides **mock mode**. ::::important Mock mode is **not** a tool for testing real purchases: - It **doesn't open** App Store / Google Play purchase flows and **doesn't create** real transactions. - It **doesn't render** paywalls/onboardings created with **Adapty Paywall Builder (AdaptyUI)**. - Adapty's native modules are **completely bypassed**—even missing native SDK files in the Xcode/Android build or an invalid API key won't trigger errors. To test real purchases and Paywall Builder paywalls, use an Expo Dev Client / production build where mock mode is automatically disabled. :::: **By default**, the SDK automatically detects Expo Go and web environments and enables mock mode. You don't need to configure anything unless you want to customize the mock data. When mock mode is active: - All Adapty methods return mock data without making network requests to Adapty's servers. - By default, the initial mock profile has no active subscriptions. - By default, `makePurchase(...)` simulates a successful purchase and grants premium access. You can customize the mock data using `mockConfig` during activation. See the config format and supported parameters [here](https://react-native.adapty.io/interfaces/adaptymockconfig). ```typescript showLineNumbers title="App.tsx" try { await adapty.activate('YOUR_PUBLIC_SDK_KEY', { mockConfig: { // Customize the initial mock profile (optional) }, }); } catch (error) { console.error('Failed to activate Adapty SDK:', error); } ``` If you need to call SDK methods before activation (such as `isActivated()` or `setLogLevel()`), use `enableMock()` before `activate()`. If the bridge is already initialized, this method does nothing. ```typescript showLineNumbers title="App.tsx" adapty.enableMock(); // Optional: pass mockConfig to customize mock data // Now you can call methods before activation await adapty.activate('YOUR_PUBLIC_SDK_KEY'); ``` #### Delay SDK activation for development purposes Adapty pre-fetches all necessary user data upon SDK activation, enabling faster access to fresh data. However, this may pose a problem in the iOS simulator, which frequently prompts for authentication during development. Although Adapty cannot control the StoreKit authentication flow, it can defer the requests made by the SDK to obtain fresh user data. By enabling the `__debugDeferActivation` property, the activate call is held until you make the next Adapty SDK call. This prevents unnecessary prompts for authentication data if not needed. It's important to note that **this feature is intended for development use only**, as it does not cover all potential user scenarios. In production, activation should not be delayed, as real devices typically remember authentication data and do not repeatedly prompt for credentials. Here's the recommended approach for usage: ```typescript showLineNumbers title="Typescript" try { adapty.activate('PUBLIC_SDK_KEY', { __debugDeferActivation: isSimulator(), // 'isSimulator' from any 3rd party library }); } catch (error) { console.error('Failed to activate Adapty SDK:', error); // Handle the error appropriately for your app } ``` #### Troubleshoot SDK activation errors on React Native's Fast Refresh When developing with the Adapty SDK in React Native, you may encounter the error: `Adapty can only be activated once. Ensure that the SDK activation call is not made more than once.` This occurs because React Native's fast refresh feature triggers multiple activation calls during development. To prevent this, use the `__ignoreActivationOnFastRefresh` option set to `__DEV__` (React Native's development mode flag). ```typescript showLineNumbers title="Typescript" try { adapty.activate('PUBLIC_SDK_KEY', { __ignoreActivationOnFastRefresh: __DEV__, }); } catch (error) { console.error('Failed to activate Adapty SDK:', error); // Handle the error appropriately for your app } ``` ## Troubleshooting #### Minimum iOS version error When building for iOS, you may see an error about the **minimum iOS version** or deployment target, especially if you use paywalls created in the [Adapty paywall builder](adapty-paywall-builder.md), which require **iOS 15.0+**. Because Expo generates the iOS project (including the `Podfile`) during `expo prebuild`, **you should not edit the `Podfile` directly**. Instead, configure the deployment target via the `expo-build-properties` config plugin. 1. Install the plugin: ```sh npx expo install expo-build-properties ``` 2. Update your Expo config (`app.json` or `app.config.js`) to set the iOS deployment target: ``` { "expo": { // ...other Expo config... "plugins": [ [ "expo-build-properties", { "ios": { // Use "13.0" for core Adapty features only, // or "15.0" if you use paywalls created in the paywall builder. "deploymentTarget": "15.0" } } ], ] } } ``` 3. Regenerate the native iOS project and rebuild: ``` npx expo prebuild --clean npx expo run:ios # or `eas build -p ios` on your CI ``` #### Android Auto Backup manifest conflict When using Expo with multiple SDKs that configure Android Auto Backup (such as Adapty, AppsFlyer, or expo-secure-store), you may encounter a manifest merger conflict. A typical error looks like this: `Manifest merger failed : Attribute application@fullBackupContent value=(@xml/secure_store_backup_rules) from AndroidManifest.xml:24:248-306 is also present at [io.adapty:android-sdk:3.12.0] AndroidManifest.xml:9:18-70 value=(@xml/adapty_backup_rules).` To resolve this conflict, you need to let the Adapty plugin manage Android backup configuration. If your project also uses `expo-secure-store`, disable its own backup setup to avoid overlap. Here’s how to configure your `app.json`: ```json title="app.json" { "expo": { "plugins": [ ["react-native-adapty", { "replaceAndroidBackupConfig": true }], ["expo-secure-store", { "configureAndroidBackup": false }] ] } } ``` The `replaceAndroidBackupConfig` option is `false` by default. When enabled, it lets the Adapty plugin control Android backup rules. Include `"configureAndroidBackup": false` if you use `expo-secure-store` to prevent warnings, since SecureStore’s backup configuration will now be handled by Adapty. :::important This setup only respects backup requirements for Adapty, AppsFlyer, and expo-secure-store. If other libraries in your project define custom backup rules, you’ll need to configure those manually. ::: --- # File: sdk-installation-react-native-pure --- --- title: "Install & configure Adapty SDK in a pure React Native project" description: "Step-by-step guide on installing Adapty SDK on React Native for subscription-based apps." --- :::important This guide applies only to **pure React Native (non-Expo) projects**. If you’re using **Expo**, follow the [Expo installation guide](sdk-installation-react-native-expo.md) instead. ::: Adapty SDK includes two key modules for seamless integration into your React Native app: - **Core Adapty**: This module is required for Adapty to function properly in your app. - **AdaptyUI**: This module is needed if you use the [Adapty Paywall Builder](adapty-paywall-builder), a user-friendly, no-code tool for easily creating cross-platform paywalls. AdaptyUI is automatically activated along with the core module. :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ## Requirements The Adapty React Native SDK supports iOS 13.0+, but using paywalls created in the [Adapty paywall builder](adapty-paywall-builder.md) requires iOS 15.0+. :::info Adapty is compatible with Google Play Billing Library up to 8.x. By default, Adapty works with Google Play Billing Library v.7.0.0 but, if you want to force a later version, you can manually [add the dependency](https://developer.android.com/google/play/billing/integrate#dependency). ::: ## Install Adapty SDK [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-React-Native.svg?style=flat&logo=react)](https://github.com/adaptyteam/AdaptySDK-React-Native/releases) 1. Install Adapty SDK (this also installs `@adapty/core` automatically): ```sh showLineNumbers title="Shell" # using npm npm install react-native-adapty # or using yarn yarn add react-native-adapty ``` 2. For iOS, install pods: ```sh showLineNumbers title="Shell" cd ios && pod install ```
For Android, if your React Native version is earlier than 0.73.0 (click to expand) Update the `/android/build.gradle` file. Make sure there is the `kotlin-gradle-plugin:1.8.0` dependency or a newer one: ```groovy showLineNumbers title="/android/build.gradle" ... buildscript { ... dependencies { ... classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.0" } } ... ```
## Activate Adapty module of Adapty SDK To get your **Public SDK Key**: 1. Go to Adapty Dashboard and navigate to [**App settings → General**](https://app.adapty.io/settings/general). 2. From the **Api keys** section, copy the **Public SDK Key** (NOT the Secret Key). 3. Replace `"YOUR_PUBLIC_SDK_KEY"` in the code. :::important - Make sure you use the **Public SDK key** for Adapty initialization, the **Secret key** should be used for [server-side API](getting-started-with-server-side-api) only. - **SDK keys** are unique for every app, so if you have multiple apps make sure you choose the right one. ::: Copy the following code to `App.tsx` to activate Adapty: ```typescript showLineNumbers title="App.tsx" adapty.activate('YOUR_PUBLIC_SDK_KEY'); ``` :::tip To avoid activation errors in the development environment, use the [tips](#development-environment-tips). ::: Now set up paywalls in your app: - If you use [Adapty Paywall Builder](adapty-paywall-builder), follow the [Paywall Builder quickstart](react-native-quickstart-paywalls). - If you build your own paywall UI, see the [quickstart for custom paywalls](react-native-quickstart-manual). ## Activate AdaptyUI module of Adapty SDK If you plan to use [Paywall Builder](adapty-paywall-builder.md), you need the AdaptyUI module. It is activated automatically when you activate the core module; you don't need to do anything else. ## Optional setup ### Logging #### Set up the logging system Adapty logs errors and other important information to help you understand what is going on. There are the following levels available: | Level | Description | | ---------- | ------------------------------------------------------------ | | `error` | Only errors will be logged | | `warn` | Errors and messages from the SDK that do not cause critical errors, but are worth paying attention to will be logged | | `info` | Errors, warnings, and various information messages will be logged | | `verbose` | Any additional information that may be useful during debugging, such as function calls, API queries, etc. will be logged | You can set the log level in your app before or during Adapty configuration: ```typescript showLineNumbers title="App.tsx" // Set log level before activation // 'verbose' is recommended for development and the first production release adapty.setLogLevel('verbose'); // Or set it during configuration adapty.activate('YOUR_PUBLIC_SDK_KEY', { logLevel: 'verbose', }); ``` ### Data policies Adapty doesn't store personal data of your users unless you explicitly send it, but you can implement additional data security policies to comply with the store or country guidelines. #### Disable IP address collection and sharing When activating the Adapty module, set `ipAddressCollectionDisabled` to `true` to disable user IP address collection and sharing. The default value is `false`. Use this parameter to enhance user privacy, comply with regional data protection regulations (like GDPR or CCPA), or reduce unnecessary data collection when IP-based features aren't required for your app. ```typescript showLineNumbers title="App.tsx" adapty.activate('YOUR_PUBLIC_SDK_KEY', { ipAddressCollectionDisabled: true, }); ``` #### Disable advertising ID collection and sharing When activating the Adapty module, set `ios.idfaCollectionDisabled` (iOS) or `android.adIdCollectionDisabled` (Android) to `true` to disable the collection of advertising identifiers. The default value is `false`. Use this parameter to comply with App Store/Play Store policies, avoid triggering the App Tracking Transparency prompt, or if your app does not require advertising attribution or analytics based on advertising IDs. ```typescript showLineNumbers title="App.tsx" adapty.activate('YOUR_PUBLIC_SDK_KEY', { ios: { idfaCollectionDisabled: true, }, android: { adIdCollectionDisabled: true, }, }); ``` #### Set up media cache configuration for AdaptyUI By default, AdaptyUI caches media (such as images and videos) to improve performance and reduce network usage. You can customize the cache settings by providing a custom configuration. Use `mediaCache` to override the default cache settings: ```typescript adapty.activate('YOUR_PUBLIC_SDK_KEY', { mediaCache: { memoryStorageTotalCostLimit: 200 * 1024 * 1024, // Optional: memory cache size in bytes memoryStorageCountLimit: 2147483647, // Optional: max number of items in memory diskStorageSizeLimit: 200 * 1024 * 1024, // Optional: disk cache size in bytes }, }); ``` Parameters: | Parameter | Required | Description | |-----------|----------|-------------| | memoryStorageTotalCostLimit | optional | Total cache size in memory in bytes. Defaults to platform-specific value. | | memoryStorageCountLimit | optional | The item count limit of the memory storage. Defaults to platform-specific value. | | diskStorageSizeLimit | optional | The file size limit on disk in bytes. Defaults to platform-specific value. | ### Enable local access levels (Android) By default, [local access levels](local-access-levels.md) are enabled on iOS and disabled on Android. To enable them on Android as well, set `localAccessLevelAllowed` to `true`: ```typescript showLineNumbers title="App.tsx" adapty.activate('YOUR_PUBLIC_SDK_KEY', { android: { localAccessLevelAllowed: true, }, }); ``` ### Clear data on backup restore When `clearDataOnBackup` is set to `true`, the SDK detects when the app is restored from an iCloud backup and deletes all locally stored SDK data, including cached profile information, product details, and paywalls. The SDK then initializes with a clean state. Default value is `false`. :::note Only local SDK cache is deleted. Transaction history with Apple and user data on Adapty servers remain unchanged. ::: ```typescript showLineNumbers title="App.tsx" adapty.activate('YOUR_PUBLIC_SDK_KEY', { ios: { clearDataOnBackup: true }, }); ``` ## Development environment tips #### Delay SDK activation for development purposes Adapty pre-fetches all necessary user data upon SDK activation, enabling faster access to fresh data. However, this may pose a problem in the iOS simulator, which frequently prompts for authentication during development. Although Adapty cannot control the StoreKit authentication flow, it can defer the requests made by the SDK to obtain fresh user data. By enabling the `__debugDeferActivation` property, the activate call is held until you make the next Adapty SDK call. This prevents unnecessary prompts for authentication data if not needed. It's important to note that **this feature is intended for development use only**, as it does not cover all potential user scenarios. In production, activation should not be delayed, as real devices typically remember authentication data and do not repeatedly prompt for credentials. Here's the recommended approach for usage: ```typescript showLineNumbers title="Typescript" try { adapty.activate('PUBLIC_SDK_KEY', { __debugDeferActivation: isSimulator(), // 'isSimulator' from any 3rd party library }); } catch (error) { console.error('Failed to activate Adapty SDK:', error); // Handle the error appropriately for your app } ``` #### Troubleshoot SDK activation errors on React Native's Fast Refresh When developing with the Adapty SDK in React Native, you may encounter the error: `Adapty can only be activated once. Ensure that the SDK activation call is not made more than once.` This occurs because React Native's fast refresh feature triggers multiple activation calls during development. To prevent this, use the `__ignoreActivationOnFastRefresh` option set to `__DEV__` (React Native's development mode flag). ```typescript showLineNumbers title="Typescript" try { adapty.activate('PUBLIC_SDK_KEY', { __ignoreActivationOnFastRefresh: __DEV__, }); } catch (error) { console.error('Failed to activate Adapty SDK:', error); // Handle the error appropriately for your app } ``` #### Set up mock mode for local testing For local development and testing, you can enable mock mode to avoid needing sandbox App Store/Google Play accounts and speed up iteration. Mock mode completely bypasses Adapty's native modules and returns simulated data. :::important Mock mode is **not** a tool for testing real purchases: - It **doesn't open** App Store / Google Play purchase flows and **doesn't create** real transactions. - It **doesn't render** paywalls/onboardings created with **Adapty Paywall Builder (AdaptyUI)**. - Adapty's native modules are **completely bypassed**—even missing native SDK files in the Xcode/Android build or an invalid API key won't trigger errors. - No data is sent to Adapty's servers. To test real purchases and Paywall Builder paywalls, disable mock mode and use sandbox accounts. ::: To enable mock mode, set `enableMock` to `true`: ```typescript showLineNumbers title="App.tsx" adapty.activate('YOUR_PUBLIC_SDK_KEY', { enableMock: true, }); ``` When mock mode is active: - All Adapty methods return mock data without making network requests to Adapty's servers. - By default, the initial mock profile has no active subscriptions. - By default, `makePurchase(...)` simulates a successful purchase and grants premium access. You can customize the mock data using `mockConfig` during activation. See the config format and supported parameters [here](https://react-native.adapty.io/interfaces/adaptymockconfig). ```typescript showLineNumbers title="App.tsx" try { await adapty.activate('YOUR_PUBLIC_SDK_KEY', { mockConfig: { // Customize the initial mock profile (optional) }, }); } catch (error) { console.error('Failed to activate Adapty SDK:', error); } ``` If you need to call SDK methods before activation (such as `isActivated()` or `setLogLevel()`), use `enableMock()` before `activate()`. If the bridge is already initialized, this method does nothing. ```typescript showLineNumbers title="App.tsx" adapty.enableMock(); // Optional: pass mockConfig to customize mock data // Now you can call methods before activation await adapty.activate('YOUR_PUBLIC_SDK_KEY'); ``` ## Troubleshooting #### Minimum iOS version error If you get a minimum iOS version error, update your Podfile: ```diff -platform :ios, min_ios_version_supported +platform :ios, '13.0' # For core features only # OR +platform :ios, '15.0' # If using paywalls created in the paywall builder ``` #### Android Auto Backup manifest conflict Some SDKs (including Adapty) ship their own Android Auto Backup configuration. If you use multiple SDKs that define backup rules, the Android manifest merger can fail with an error mentioning `android:fullBackupContent`, `android:dataExtractionRules`, or `android:allowBackup`. Typical error symptoms: `Manifest merger failed: Attribute application@dataExtractionRules value=(@xml/your_data_extraction_rules) is also present at [com.other.sdk:library:1.0.0] value=(@xml/other_sdk_data_extraction_rules)` :::note These changes should be made in your Android platform directory (typically located in your project's `android/` folder). ::: To resolve this, you need to: - Tell the manifest merger to use your app's values for backup-related attributes. - Create backup rule files that merge Adapty's rules with rules from other SDKs. #### 1. Add the `tools` namespace to your manifest In your `AndroidManifest.xml` file, ensure the root `` tag includes tools: ```xml ... ``` #### 2. Override backup attributes in `` In the same `AndroidManifest.xml` file, update the `` tag so that your app provides the final values and tells the manifest merger to replace library values: ```xml ... ``` If any SDK also sets `android:allowBackup`, include it in `tools:replace` as well: ```xml tools:replace="android:allowBackup,android:fullBackupContent,android:dataExtractionRules" ``` #### 3. Create merged backup rules files Create XML files in your Android project's `res/xml/` directory that combine Adapty's rules with rules from other SDKs. Android uses different backup rule formats depending on the OS version, so creating both files ensures compatibility across all Android versions your app supports. :::note The examples below show AppsFlyer as a sample third-party SDK. Replace or add rules for any other SDKs you're using in your app. ::: **For Android 12 and higher** (uses the new data extraction rules format): ```xml title="sample_data_extraction_rules.xml" ``` **For Android 11 and lower** (uses the legacy full backup content format): ```xml title="sample_backup_rules.xml" #### Purchases fail after returning from another app in Android If the Activity that starts the purchase flow uses a non-default `launchMode`, Android may recreate or reuse it incorrectly when the user returns from Google Play, a banking app, or a browser. This can cause the purchase result to be lost or treated as canceled. To ensure purchases work correctly, use only `standard` or `singleTop` launch modes for the Activity that starts the purchase flow, and avoid any other modes. In your `AndroidManifest.xml`, ensure the Activity that starts the purchase flow is set to `standard` or `singleTop`: ```xml ``` --- # File: react-native-quickstart-paywalls --- --- title: "Enable purchases by using paywalls in React Native SDK" description: "Learn how to present paywalls in your React Native app with Adapty SDK." --- To enable in-app purchases, you need to understand three key concepts: - [**Products**](product.md) – anything users can buy (subscriptions, consumables, lifetime access) - [**Paywalls**](paywalls.md) are configurations that define which products to offer. In Adapty, paywalls are the only way to retrieve products, but this design lets you modify offerings, pricing, and product combinations without touching your app code. - [**Placements**](placements.md) – where and when you show paywalls in your app (like `main`, `onboarding`, `settings`). You set up paywalls for placements in the dashboard, then request them by placement ID in your code. This makes it easy to run A/B tests and show different paywalls to different users. Adapty offers you three ways to enable purchases in your app. Select one of them depending on your app requirements: | Implementation | Complexity | When to use | |---------------------------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Adapty Paywall Builder | ✅ Easy | You [create a complete, purchase-ready paywall in the no-code builder](quickstart-paywalls). Adapty automatically renders it and handles all the complex purchase flow, receipt validation, and subscription management behind the scenes. | | Manually created paywalls | 🟡 Medium | You implement your paywall UI in your app code, but still get the paywall object from Adapty to maintain flexibility in product offerings. See the [guide](react-native-quickstart-manual). | | Observer mode | 🔴 Hard | You already have your own purchase handling infrastructure and want to keep using it. Note that the observer mode has its limitations in Adapty. See the [article](observer-vs-full-mode). | :::important **The steps below show how to implement a paywall created in the Adapty paywall builder.** If you don't want to use the paywall builder, see the [guide for handling purchases in manually created paywalls](react-native-making-purchases.md). ::: To display a paywall created in the Adapty paywall builder, in your app code, you only need to: 1. **Get the paywall**: Get the paywall from Adapty. 2. **Display the paywall and Adapty will handle purchases for you**: Show the paywall container you've got in your app. 3. **Handle button actions**: Associate user interactions with the paywall with your app's response to them. For example, open links or close the paywall when users click buttons. ## Before you start Before you start, complete these steps: 1. Connect your app to the [App Store](initial_ios) and/or [Google Play](initial-android) in the Adapty Dashboard. 2. [Create your products](create-product) in Adapty. 3. [Create a paywall and add products to it](create-paywall). 4. [Create a placement and add your paywall to it](create-placement). 5. [Install and activate the Adapty SDK](sdk-installation-reactnative) in your app code. :::tip The fastest way to complete these steps is to follow the [quickstart guide](quickstart). ::: ## 1. Get the paywall Your paywalls are associated with placements configured in the dashboard. Placements allow you to run different paywalls for different audiences or to run [A/B tests](ab-tests.md). To get a paywall created in the Adapty paywall builder, you need to: 1. Get the `paywall` object by the [placement](placements.md) ID using the `getPaywall` method and check whether it is a paywall created in the builder using the `hasViewConfiguration` property. 2. Create the paywall view using the `createPaywallView` method. The view contains the UI elements and styling needed to display the paywall. :::important To get the view configuration, you must switch on the **Show on device** toggle in the Paywall Builder. Otherwise, you will get an empty view configuration, and the paywall won't be displayed. ::: ```typescript showLineNumbers title="React Native" try { const placementId = 'YOUR_PLACEMENT_ID'; const paywall = await adapty.getPaywall(placementId); // the requested paywall } catch (error) { // handle the error } if (paywall.hasViewConfiguration) { try { const view = await createPaywallView(paywall); } catch (error) { // handle the error } } else { //use your custom logic } ``` ## 2. Display the paywall Now, when you have the paywall configuration, it's enough to add a few lines to display your paywall. To embed a paywall within your existing component tree, use the `AdaptyPaywallView` component directly in your React Native component hierarchy: ```typescript showLineNumbers title="React Native (TSX)" function MyPaywall({ paywall }) { const onCloseButtonPress = useCallback(() => {}, []); const onUrlPress = useCallback((url) => { Linking.openURL(url); }, []); return ( ); } ``` To display the paywall as a standalone screen, use the `view.present()` method on the `view` created by the `createPaywallView` method. Each `view` can only be used once. If you need to display the paywall again, call `createPaywallView` one more to create a new `view` instance. ```typescript showLineNumbers title="React Native" try { await view.present(); } catch (error) { // handle the error } ``` :::tip For more details on how to display a paywall, see our [guide](react-native-present-paywalls.md). ::: ## 3. Handle button actions When users click buttons in the paywall, the React Native SDK automatically handles purchases, restoration, closing the paywall, and opening URLs. However, other buttons have custom or pre-defined IDs and require handling actions in your code. Or, you may want to override their default behavior. For example, here is the default behavior for the close button. You don't need to add it in the code, but here, you can see how it is done if needed. For React component, handle actions directly in the `AdaptyPaywallView` component: ```typescript showLineNumbers title="React Native (TSX)" function MyPaywall({ paywall }) { const onUrlPress = useCallback((url) => { Linking.openURL(url); }, []); const onCloseButtonPress = useCallback(() => {}, []); const onCustomAction = useCallback((actionId) => {}, []); return ( ); } ``` For modal presentation, implement event handlers using `setEventHandlers`: ```typescript showLineNumbers title="React Native" const unsubscribe = view.setEventHandlers({ onCloseButtonPress() { return true; // allow paywall closing } }); ``` :::tip Read our guides on how to handle button [actions](react-native-handle-paywall-actions.md) and [events](react-native-handling-events-1.md). ::: ## Next steps Your paywall is ready to be displayed in the app. Test your purchases in the [App Store sandbox](test-purchases-in-sandbox) or in [Google Play Store](testing-on-android) to make sure you can complete a test purchase from the paywall. Now, you need to [check the users' access level](react-native-check-subscription-status.md) to ensure you display a paywall or give access to paid features to right users. ## Full example Here is how all those steps can be integrated in your app together. ```javascript showLineNumbers title="React Native (TSX)" export default function PaywallScreen() { const [paywall, setPaywall] = useState(null); const loadPaywall = async () => { try { const paywallData = await adapty.getPaywall('YOUR_PLACEMENT_ID'); if (paywallData.hasViewConfiguration) { setPaywall(paywallData); } } catch (error) { console.warn('Error loading paywall:', error); } }; const onUrlPress = useCallback((url) => { Linking.openURL(url); }, []); const onCloseButtonPress = useCallback(() => { // Handle close button press }, []); useEffect(() => { loadPaywall(); }, []); return ( {paywall ? ( ) : (