# Adapty Documentation - Full Content This file contains the complete content of all documentation pages. Generated on: 2025-09-02T07:17:19.922Z Total files: 520 --- # File: InvalidProductIdentifiers-flutter.md --- --- title: "Fix for Code-1000 noProductIDsFound error in Flutter 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. 4. Paste the copied value to the **Bundle 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: InvalidProductIdentifiers-react-native.md --- --- title: "Fix for Code-1000 noProductIDsFound error in React Native 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. 4. Paste the copied value to the **Bundle 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: InvalidProductIdentifiers-unity.md --- --- title: "Fix for Code-1000 noProductIDsFound error in Unity 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. 4. Paste the copied value to the **Bundle 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: InvalidProductIdentifiers.md --- --- 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. 4. Paste the copied value to the **Bundle 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: ab-tests.md --- --- title: "A/B test" description: "Optimize subscription pricing with A/B tests in Adapty for better conversion rates." --- Boost in-app purchases and subscription revenue by running A/B tests in Adapty. You can experiment with pricing, subscription lengths, trial periods, and more—no code changes needed. This guide shows how to create A/B tests in the Adapty Dashboard and how to read the results so you can make data-driven decisions about monetization. :::warning Be sure you [send paywall views to Adapty](present-remote-config-paywalls#track-paywall-view-events) using the `.logShowPaywall().` Without this method, Adapty wouldn't be able to calculate views for the paywalls within the test, which would result in irrelevant conversion stats. ::: ## A/B test types Adapty offers three A/B test types: - **Regular A/B test:** An A/B test created for a single [paywall](https://adapty.io/docs/paywalls) placement. - **Onboarding A/B test:** An A/B test created for a single [onboarding](https://adapty.io/docs/onboardings) placement. - **Crossplacement A/B test:** An A/B test created for multiple paywall placements in your app. Once a A/B test variants are alternative versions of the paywall or onboarding to test. is assigned by the A/B test, it will be consistently shown across all selected sections of your app. :::warning Crossplacement A/B tests are only available for native iOS, Flutter, and Android SDKs starting from v3.5.0. Onboarding A/B tests require the native iOS or Android SDK version 3.8.0 or higher. Users from previous versions skip them. ::: ### A/B test types use cases Each A/B test type is useful if: - **Regular** and **Onboarding A/B/ tests**: - You have only one placement in your app. - You want to run your A/B test for only one placement even if you have multiple placements in your app and see economics changes for this one placement only. - You want to run an A/B test on old users (those who have seen at least one Adapty paywall). - **Crossplacement A/B test**: - You want to synchronize variants used across multiple placements—e.g., if you want to change prices in the onboarding flow and in your app’s settings the same time. - You want to evaluate your app's overall economy, ensuring that A/B testing is conducted across the entire app rather than just specific parts, making it easier to analyze results in the A/B testing statistics. - You want to run an A/B test on new users only, i.e. the users who have never seen a single Adapty paywall. - You want to use multiple paywalls within a single variant: ### Key differences | Feature | Regular A/B Test | Crossplacement A/B Test | | ------------------------------- |--------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------| | **What is being tested** | One paywall/onboarding | Set of paywalls belonging to one variant | | **Variant consistency** | Variant is determined separately for every placement | Same variant used across all paywall placements | | **Audience targeting** | Defined per paywall/onboarding placement | Shared across all paywall placements | | **Analytics** | You analyse one paywall/onboarding placement | You analyze the whole app on those placements that are a part of the test | | **Variant weight distribution** | Per paywall/onboarding | Per set of paywalls | | **Users** | For all users | Only new users (those who haven’t seen an Adapty paywall) | | **Adapty SDK version** | Any for paywalls. Except :warning: 3.8.0+ for onboardings | :warning: 3.5.0+ | | **Best for** | Testing independent changes in a single paywall/onboarding placement without considering the overall app economics | Evaluating overall monetization strategies app-wide | | Feature | Regular A/B test | Cross-placement A/B test | | --- | --- | --- | | **What is tested** | One paywall/onboarding | Set of paywalls in one variant | | **Variant consistency** | Defined per placement | Same variant across all placements | | **Audience targeting** | Per placement | Shared across placements | | **Analytics** | You analyze one placement | You analyze the whole app (placements in the test) | | **Variant weight** | Per placement | Per variant | | **Users** | All users | Only new users (those who haven’t seen an Adapty paywall) | | **Adapty SDK version** | Any for paywalls
:warning: 3.8.0+ for onboardings | :warning: 3.5.0+ | | **Best for** | Independent changes in a single placement | App-wide monetization strategy | Each paywall/onboarding gets a weight that splits traffic during the test. For instance, with weights of 70 % and 30 %, the first paywall is shown to roughly 700 of 1,000 users, the second to about 300. In cross-placement tests, weights are set per variant, not per paywall. This setup allows you to effectively compare different paywalls and make smarter, data-driven decisions for your app's monetization strategy. ## A/B test selection logic As you may have noticed from the table above, **cross-placement A/B tests take priority over regular A/B tests**. However, cross-placement tests are only shown to **new users** — those who haven’t seen a single Adapty paywall yet (to be precise, `getPaywall` SDK method was called). This ensures consistency in results across placements. Here is an example of the logic Adapty follows when deciding which A/B test to show for a paywall placement: Regular, onboarding, and cross-placement tests appear on separate tabs that you can switch between. ## Crossplacement A/B test limitations Currently, crossplacement A/B tests cannot include onboarding placements. Crossplacement A/B tests guarantee the same variant across all placements in the A/B test, but this creates several limitations: - They always have the highest priority in a placement. - Only new users can participate, i.e. the users who have not seen a single Adapty paywall before (to be precise, `getPaywall` SDK method was called). That is done because it's not possible to guarantee for the old users that they will see the same paywall chain, because an existing user could have seen something before the test has been started. ## Creating A/B tests When creating a new A/B test, you need to include at least two [paywalls](paywalls) or [onboardings](https://adapty.io/docs/onboardings), depending on your test type. To create a new A/B test: 1. Go to [A/B tests](ab-tests) from the Adapty main menu. 3. Click **Create A/B test** at the top right. 3. In the **Create the A/B test** window, enter a **Test name**. This is required and should help you easily identify the test later. Choose something descriptive and meaningful so it's clear what the test is about when you review the results. 4. Fill in the **Test goal** to keep track of what you're trying to achieve. This could be increasing subscriptions, improving engagement, reducing churn, or anything else you're focusing on. A clear goal helps you stay on track and measure success. 5. Click **Select placement** and choose a paywall placement for a regular A/B test or an onboarding placement for an onboarding A/B test. 6. Set up the test content using the **Variants** table. Variants go in rows, placements go in columns, and paywalls are added where they intersect. By default, there are 2 variants and 1 placement, but you can add more of each. Once you add a second placement, the test becomes a cross-placement A/B test. Key:
1 Rename the variant to make it more descriptive for you.
2 Change the weight of the variant. Keep in mind that the total of all variants must equal 100%.
3 Add more variants if needed.
4 Add more placements if needed.
5 Add paywalls/onboardings to display in the placements for every variant.
7. Save your test. You have two options: 1. **Save as draft**: The test won't go live right away. You can launch it later from either the placement or A/B test list. This option is ideal if you're not ready to start the test and want more time to review or make adjustments. 2. **Run A/B test**: Choose this if you're ready to launch the test immediately. The test will go live as soon as you click this button. To learn more about launching A/B tests, check out our [guide on running A/B tests](run_stop_ab_tests). You can also track performance using a variety of metrics—see the [metrics documentation](results-and-metrics) for more details. ## Editing A/B tests You can only edit A/B tests that are saved as drafts. Once a test is live, it cannot be changed. However, you can use the **Modify** option to create a duplicate of the test with the same name and make your updates there. The original test will be stopped, and both the original and new versions will appear separately in your analytics. --- # File: access-level.md --- --- title: "Access levels" description: "Learn about access levels in Adapty and how to configure them for user management." --- Access levels let you control what users can do in your mobile app without hardcoding specific product IDs. Each product defines how long the user gets a certain access level for. So, whenever a user makes a purchase, Adapty grants access to the app for a specific period (for subscriptions) or forever (for lifetime purchases). When you create an app in the Adapty Dashboard, the `premium` access level is automatically generated. This serves as the default access level, and it cannot be deleted. You can have multiple access levels per app. Here are some examples of when they can be useful: - In a newspaper app where you sell subscriptions to different topics independently, you can create access levels such as `sports` and `science`. - In a fitness app offering recorded video training under a regular subscription (using the default `premium` access level), customers may opt for a more expensive option providing access to live training with a coach. In this case, you can create a `live_coach_access` level. - In a language learning app, you can choose to create an access level for each available language. To begin working with access levels in Adapty, open the **[Paywalls and Products](https://app.adapty.io/access-levels)** section from the Adapty main menu, then select the **Access levels** tab. The **Access levels** list displays all access levels, including the `premium` one that is added automatically and those added by you in Adapty. --- # File: account.md --- --- title: "Account details" description: "Manage your Adapty account and optimize settings for better subscription tracking." --- Your account page can be accessed through the link in the top right corner after you've logged into Adapty. Or just by [this link](https://app.adapty.io/account). There are several important things you can set on your account page, so let's go through them. ## General settings Here you fill in your name and the name of your company. It's useful to have this information when you contact support, so they can work with your correctly filled profile. Your main credentials are also on this part of the page. So you may change your password here or check what email your account is registered for. ## Date and time format As part of general settings, the date and time format deserves to be highlighted separately because it can make your life with Adapty easier. By selecting the option, you can choose between American or European format to display dates and times in your preferred way. For example, if you prefer January 31, 2022 date format and it's common for you to use AM and PM in time, your choice is the American format. And vice versa, if it's common for you to use 31 January, 2022 date format and see the time as 16:00, then your option is European format. ## Members You can manage your team members in your account settings. Please [read more about it](members-settings). ## Billing info You can edit your payment method and upgrade your plan on the account page. --- # File: active-subscriptions.md --- --- title: "Active subscriptions" description: "Monitor and manage active subscriptions with Adapty's robust analytics." --- The Active subscriptions chart displays the amount of unique paid subscriptions that have not yet expired at the end of each selected period. It includes regular (unexpired) in-app subscriptions that started and are currently active and excludes both free trials and subscriptions with canceled renewals. ### Calculation Adapty's Active Subscriptions calculation logic counts the number of paid, unexpired subscriptions at the end of a given period. At a daily resolution, the amount of Active subscriptions represents the number of unexpired subscriptions at the end of that day. Therefore, the count of Active subscriptions for a given day indicates the number of unexpired subscriptions at the end of that day. At a monthly resolution, the count of Active Subscriptions represents the number of unexpired subscriptions at the end of that month. Note that, a paid subscription without a grace period is considered expired once its next renewal date has passed without a successful renewal. For example, if there were 500 active subscriptions at the end of last month, 50 new subscriptions were started this month, and 25 subscriptions expired this month, then there are 525 active subscriptions at the end of this month. ### Available filters and grouping - ✅ Filter by: Attribution, country, paywall, store, product, and duration. - ✅ Group by: Product, country, store, paywall, duration, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative. You can find more information about the available controls, filters, grouping options, and how to use them in [this documentation.](controls-filters-grouping-compare-proceeds) ### Active subscriptions chart usage The Active subscriptions chart is useful to get valuable insights into the number of recurring, individual paid users from your app. This metric serves as a proxy for the size and growth potential of a business. Combining Active Subscriptions with filters and grouping helps you to gain a deeper understanding of their paid subscriber base composition, making it a powerful tool for data analysis. ### Similar metrics In addition to Active subscriptions, Adapty also provides metrics for other subscription-related events, such as new subscriptions, subscriptions renewal canceled, expired subscriptions, and non-subscriptions. To learn more about these subscriptions-related metrics, please refer to the following documentation: - [Churned (expired) subscriptions](churned-expired-subscriptions) - [Cancelled subscriptions](cancelled-subscriptions) - [Non-subscriptions](non-subscriptions) --- # File: active-trials.md --- --- title: "Active trials" description: "Track and manage active subscription trials with Adapty analytics." --- The active trials chart in Adapty displays the number of unexpired free trials that are currently active at the end of a given period. Active means subscriptions that have not yet expired; therefore, users still have access to the paid features of the app. ### Calculation Adapty calculates the number of active trials in a given period by referring to the count of unexpired free trials by the end of that period. This count remains unchanged until the trial expires, regardless of its auto-renew status. At a daily resolution, the count of Active Trials represents the number of unexpired trials by the end of that day. For example, if 100 trials were active yesterday, 10 new trials were activated today, and 5 trials have expired today, then there are 105 active trials today. However, at a monthly resolution, the count of Active Trials represents the number of unexpired trials by the end of that month. ### Available filters and grouping - ✅ Filter by: Attribution, country, paywall, store, product, and duration. - ✅ Group by: Renewal status, period, product, country, store, paywall, duration, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative. You can find more information about the available controls, filters, grouping options, and how to use them in [this documentation.](controls-filters-grouping-compare-proceeds) ### Active trials chart usage This chart provides valuable insights into the effectiveness of your app's trial offers and allows you to monitor the number of users currently taking advantage of your free trial periods. By leveraging the insights provided by the Active Trials chart, you can optimize your app's free trial strategy and maximize user engagement and revenue generation. With Adapty's powerful analytics and monitoring tools, you'll have everything you need to make data-driven decisions that drive your app's success. ### Similar metrics In addition to Active Trials, Adapty also provides metrics for other trial-related events, such as New trials, Trial Renewal cancelled, and Expired trials. To learn more about these trial-related metrics, please refer to the following documentation: - [New trials](new-trials) - [Trial renewal cancelled](trials-renewal-cancelled) - [Expired trials](expired-churned-trials) --- # File: adapty-cursor-android.md --- --- title: "Use LLMs to implement Adapty" description: "Install and configure Adapty SDK in your Android project using Cursor, ChatGPT, Claude, or other AI tools." displayed_sidebar: sdkandroid --- You can use large language models (LLMs) to help you integrate Adapty into your Android app. We provide a set of tools and best practices if you use LLMs during development. ## Plain text docs You can access all of our documentation as plain text markdown files by adding `.md` to the end of any URL or clicking **View as Markdown** under article titles. To copy the whole Markdown content immediately, click **Copy for LLM**. For example, you can find the plain text version of this page itself at [https://adapty.io/docs/adapty-cursor-android.md](https://adapty.io/docs/adapty-cursor-android.md). This helps AI tools and agents consume our content and allows you to copy and paste the entire contents of a doc into an LLM. This format is preferable to scraping or copying from our HTML and JavaScript-rendered pages because: * Plain text contains fewer formatting tokens. * Content that isn't rendered in the default view (for example, it's hidden in a tab) of a given page is rendered in the plain text version. * LLMs can parse and understand markdown hierarchy. We also host the [`llms.txt` file](https://adapty.io/docs/llms.txt) which instructs AI tools and agents how to retrieve the plain text versions of our pages. The `/llms.txt` file is an [emerging standard](https://llmstxt.org/) for making websites and content more 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. :::tip Additionally, we host [`llms-full.txt`](https://adapty.io/docs/llms-full.txt). The `llms-full.txt` file combines the entire Adapty documentation site into a single file as context for AI tools and is indexed by LLM traffic. ::: ## Use with Cursor **Cursor**, the AI code editor, makes it easier to integrate and maintain Android apps with Adapty's infrastructure. This guide shows you how to configure Cursor for better results when working with Adapty's Android SDK. ### Add Adapty documentation to Cursor Adding Adapty's documentation directly to Cursor means you don't have to switch between your code editor and browser. This setup gives you quick access to the latest implementation guides and best practices while coding. To add Adapty documentation to Cursor: 1. In Cursor, go to **Settings > Cursor settings**. Switch to **Indexing & Docs**. 2. Click **Add doc**. 3. Enter the URL `https://adapty.io/docs`. 4. Name the documentation `Adapty Docs` and click **Confirm** to save it. ### Add Adapty SDKs to Cursor For platform-specific development, we recommend adding the GitHub repositories for the Adapty SDKs you're using. This gives you access to the latest code examples, changelogs, and implementation details. Follow the same process as above, but use the relevant SDK repository URL: - **iOS SDK**: `https://github.com/adaptyteam/AdaptySDK-iOS` - **Android SDK**: `https://github.com/adaptyteam/AdaptySDK-Android` - **Flutter SDK**: `https://github.com/adaptyteam/AdaptySDK-Flutter` - **React Native SDK**: `https://github.com/adaptyteam/AdaptySDK-React-Native` - **Unity SDK**: `https://github.com/adaptyteam/AdaptySDK-Unity` Name each doc `Adapty SDK` (e.g., `Adapty Android SDK`). ## Reference Adapty in your prompts Once configured, you can reference Adapty documentation and SDKs using the `@` symbol in your prompts. This makes sure Cursor uses current and accurate information when generating code. **Example prompts:** - `@Adapty Docs help me install the Adapty Android SDK` - `Using @Adapty Android SDK, show me how to handle purchase restoration with proper error handling` - `@Adapty Docs @Adapty Android SDK help me show a paywall created in the paywall builder` --- # File: adapty-cursor-flutter.md --- --- title: "Use LLMs to implement Adapty" description: "Install and configure Adapty SDK in your Flutter project using Cursor, ChatGPT, Claude, or other AI tools." displayed_sidebar: sdkflutter --- You can use large language models (LLMs) to help you integrate Adapty into your Flutter app. We provide a set of tools and best practices if you use LLMs during development. ## Plain text docs You can access all of our documentation as plain text markdown files by adding `.md` to the end of any URL or clicking **View as Markdown** under article titles. To copy the whole Markdown content immediately, click **Copy for LLM**. For example, you can find the plain text version of this page itself at [https://adapty.io/docs/adapty-cursor-flutter.md](https://adapty.io/docs/adapty-cursor-flutter.md). This helps AI tools and agents consume our content and allows you to copy and paste the entire contents of a doc into an LLM. This format is preferable to scraping or copying from our HTML and JavaScript-rendered pages because: * Plain text contains fewer formatting tokens. * Content that isn't rendered in the default view (for example, it's hidden in a tab) of a given page is rendered in the plain text version. * LLMs can parse and understand markdown hierarchy. We also host the [`llms.txt` file](https://adapty.io/docs/llms.txt) which instructs AI tools and agents how to retrieve the plain text versions of our pages. The `/llms.txt` file is an [emerging standard](https://llmstxt.org/) for making websites and content more 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. :::tip Additionally, we host [`llms-full.txt`](https://adapty.io/docs/llms-full.txt). The `llms-full.txt` file combines the entire Adapty documentation site into a single file as context for AI tools and is indexed by LLM traffic. ::: ## Use with Cursor **Cursor**, the AI code editor, makes it easier to integrate and maintain Flutter apps with Adapty's infrastructure. This guide shows you how to configure Cursor for better results when working with Adapty's Flutter SDK. ### Add Adapty documentation to Cursor Adding Adapty's documentation directly to Cursor means you don't have to switch between your code editor and browser. This setup gives you quick access to the latest implementation guides and best practices while coding. To add Adapty documentation to Cursor: 1. In Cursor, go to **Settings > Cursor settings**. Switch to **Indexing & Docs**. 2. Click **Add doc**. 3. Enter the URL `https://adapty.io/docs`. 4. Name the documentation `Adapty Docs` and click **Confirm** to save it. ### Add Adapty SDKs to Cursor For platform-specific development, we recommend adding the GitHub repositories for the Adapty SDKs you're using. This gives you access to the latest code examples, changelogs, and implementation details. Follow the same process as above, but use the relevant SDK repository URL: - **iOS SDK**: `https://github.com/adaptyteam/AdaptySDK-iOS` - **Android SDK**: `https://github.com/adaptyteam/AdaptySDK-Android` - **Flutter SDK**: `https://github.com/adaptyteam/AdaptySDK-Flutter` - **React Native SDK**: `https://github.com/adaptyteam/AdaptySDK-React-Native` - **Unity SDK**: `https://github.com/adaptyteam/AdaptySDK-Unity` Name each doc `Adapty SDK` (e.g., `Adapty Flutter SDK`). ## Reference Adapty in your prompts Once configured, you can reference Adapty documentation and SDKs using the `@` symbol in your prompts. This makes sure Cursor uses current and accurate information when generating code. **Example prompts:** - `@Adapty Docs help me install the Adapty Flutter SDK` - `Using @Adapty Flutter SDK, show me how to handle purchase restoration with proper error handling` - `@Adapty Docs @Adapty Flutter SDK help me show a paywall created in the paywall builder` --- # File: adapty-cursor-react-native.md --- --- title: "Use LLMs to implement Adapty" description: "Install and configure Adapty SDK in your React Native project using Cursor, ChatGPT, Claude, or other AI tools." slug: /adapty-cursor-react-native displayed_sidebar: sdkreactnative --- You can use large language models (LLMs) to help you integrate Adapty into your React Native app. We provide a set of tools and best practices if you use LLMs during development. ## Plain text docs You can access all of our documentation as plain text markdown files by adding `.md` to the end of any URL or clicking **View as Markdown** under article titles. To copy the whole Markdown content immediately, click **Copy for LLM**. For example, you can find the plain text version of this page itself at [https://adapty.io/docs/adapty-cursor-react-native.md](https://adapty.io/docs/adapty-cursor-react-native.md). This helps AI tools and agents consume our content and allows you to copy and paste the entire contents of a doc into an LLM. This format is preferable to scraping or copying from our HTML and JavaScript-rendered pages because: * Plain text contains fewer formatting tokens. * Content that isn't rendered in the default view (for example, it's hidden in a tab) of a given page is rendered in the plain text version. * LLMs can parse and understand markdown hierarchy. We also host the [`llms.txt` file](https://adapty.io/docs/llms.txt) which instructs AI tools and agents how to retrieve the plain text versions of our pages. The `/llms.txt` file is an [emerging standard](https://llmstxt.org/) for making websites and content more 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. :::tip Additionally, we host [`llms-full.txt`](https://adapty.io/docs/llms-full.txt). The `llms-full.txt` file combines the entire Adapty documentation site into a single file as context for AI tools and is indexed by LLM traffic. ::: ## Use with Cursor **Cursor**, the AI code editor, makes it easier to integrate and maintain React Native apps with Adapty's infrastructure. This guide shows you how to configure Cursor for better results when working with Adapty's React Native SDK. ### Add Adapty documentation to Cursor Adding Adapty's documentation directly to Cursor means you don't have to switch between your code editor and browser. This setup gives you quick access to the latest implementation guides and best practices while coding. To add Adapty documentation to Cursor: 1. In Cursor, go to **Settings > Cursor settings**. Switch to **Indexing & Docs**. 2. Click **Add doc**. 3. Enter the URL `https://adapty.io/docs`. 4. Name the documentation `Adapty Docs` and click **Confirm** to save it. ### Add Adapty SDKs to Cursor For platform-specific development, we recommend adding the GitHub repositories for the Adapty SDKs you're using. This gives you access to the latest code examples, changelogs, and implementation details. Follow the same process as above, but use the relevant SDK repository URL: - **iOS SDK**: `https://github.com/adaptyteam/AdaptySDK-iOS` - **Android SDK**: `https://github.com/adaptyteam/AdaptySDK-Android` - **Flutter SDK**: `https://github.com/adaptyteam/AdaptySDK-Flutter` - **React Native SDK**: `https://github.com/adaptyteam/AdaptySDK-React-Native` - **Unity SDK**: `https://github.com/adaptyteam/AdaptySDK-Unity` Name each doc `Adapty SDK` (e.g., `Adapty React Native SDK`). ## Reference Adapty in your prompts Once configured, you can reference Adapty documentation and SDKs using the `@` symbol in your prompts. This makes sure Cursor uses current and accurate information when generating code. **Example prompts:** - `@Adapty Docs help me install the Adapty React Native SDK` - `Using @Adapty React Native SDK, show me how to handle purchase restoration with proper error handling` - `@Adapty Docs @Adapty React Native SDK help me show a paywall created in the paywall builder` --- # File: adapty-cursor-unity.md --- --- title: "Use LLMs to implement Adapty" description: "Install and configure Adapty SDK in your Unity project using Cursor, ChatGPT, Claude, or other AI tools." slug: /adapty-cursor-unity displayed_sidebar: sdkunity --- You can use large language models (LLMs) to help you integrate Adapty into your Unity app. We provide a set of tools and best practices if you use LLMs during development. ## Plain text docs You can access all of our documentation as plain text markdown files by adding `.md` to the end of any URL or clicking **View as Markdown** under article titles. To copy the whole Markdown content immediately, click **Copy for LLM**. For example, you can find the plain text version of this page itself at [https://adapty.io/docs/adapty-cursor-unity.md](https://adapty.io/docs/adapty-cursor-unity.md). This helps AI tools and agents consume our content and allows you to copy and paste the entire contents of a doc into an LLM. This format is preferable to scraping or copying from our HTML and JavaScript-rendered pages because: * Plain text contains fewer formatting tokens. * Content that isn't rendered in the default view (for example, it's hidden in a tab) of a given page is rendered in the plain text version. * LLMs can parse and understand markdown hierarchy. We also host the [`llms.txt` file](https://adapty.io/docs/llms.txt) which instructs AI tools and agents how to retrieve the plain text versions of our pages. The `/llms.txt` file is an [emerging standard](https://llmstxt.org/) for making websites and content more 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. :::tip Additionally, we host [`llms-full.txt`](https://adapty.io/docs/llms-full.txt). The `llms-full.txt` file combines the entire Adapty documentation site into a single file as context for AI tools and is indexed by LLM traffic. ::: ## Use with Cursor **Cursor**, the AI code editor, makes it easier to integrate and maintain Unity apps with Adapty's infrastructure. This guide shows you how to configure Cursor for better results when working with Adapty's Unity SDK. ### Add Adapty documentation to Cursor Adding Adapty's documentation directly to Cursor means you don't have to switch between your code editor and browser. This setup gives you quick access to the latest implementation guides and best practices while coding. To add Adapty documentation to Cursor: 1. In Cursor, go to **Settings > Cursor settings**. Switch to **Indexing & Docs**. 2. Click **Add doc**. 3. Enter the URL `https://adapty.io/docs`. 4. Name the documentation `Adapty Docs` and click **Confirm** to save it. ### Add Adapty SDKs to Cursor For platform-specific development, we recommend adding the GitHub repositories for the Adapty SDKs you're using. This gives you access to the latest code examples, changelogs, and implementation details. Follow the same process as above, but use the relevant SDK repository URL: - **iOS SDK**: `https://github.com/adaptyteam/AdaptySDK-iOS` - **Android SDK**: `https://github.com/adaptyteam/AdaptySDK-Android` - **Flutter SDK**: `https://github.com/adaptyteam/AdaptySDK-Flutter` - **React Native SDK**: `https://github.com/adaptyteam/AdaptySDK-React-Native` - **Unity SDK**: `https://github.com/adaptyteam/AdaptySDK-Unity` Name each doc `Adapty SDK` (e.g., `Adapty Unity SDK`). ## Reference Adapty in your prompts Once configured, you can reference Adapty documentation and SDKs using the `@` symbol in your prompts. This makes sure Cursor uses current and accurate information when generating code. **Example prompts:** - `@Adapty Docs help me install the Adapty Unity SDK` - `Using @Adapty Unity SDK, show me how to handle purchase restoration with proper error handling` - `@Adapty Docs @Adapty Unity SDK help me show a paywall created in the paywall builder` --- # File: adapty-cursor.md --- --- title: "Use LLMs to implement Adapty" description: "Install and configure Adapty SDK in your project using Cursor, ChatGPT, Claude, or other AI tools." --- You can use large language models (LLMs) to help you integrate Adapty into your app. We provide a set of tools and best practices if you use LLMs during development. ## Plain text docs You can access all of our documentation as plain text Markdown files by adding `.md` to the end of any URL or clicking **View as Markdown** under article titles. To copy the whole Markdown content immediately, click **Copy for LLM**. For example, you can find the plain text version of this page itself at [https://adapty.io/docs/adapty-cursor.md](https://adapty.io/docs/adapty-cursor.md). This helps AI tools and agents consume our content and allows you to copy and paste the entire contents of a doc into an LLM. This format is preferable to scraping or copying from our HTML and JavaScript-rendered pages because: * Plain text contains fewer formatting tokens. * Content that isn't rendered in the default view (for example, it's hidden in a tab) of a given page is rendered in the plain text version. * LLMs can parse and understand markdown hierarchy. We also host the [`llms.txt` file](https://adapty.io/docs/llms.txt) which instructs AI tools and agents how to retrieve the plain text versions of our pages. The `/llms.txt` file is an [emerging standard](https://llmstxt.org/) for making websites and content more 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. :::tip Additionally, we host [`llms-full.txt`](https://adapty.io/docs/llms-full.txt). The `llms-full.txt` file combines the entire Adapty documentation site into a single file as context for AI tools and is indexed by LLM traffic. ::: ## Tips for prompting with Claude, ChatGPT, or Gemini If you’re using Claude Code, run the `init` command after launching the Code CLI so Claude can analyze your codebase. This helps it get oriented and provide better advice once you start asking questions. If you plan to integrate the Adapty SDK into your iOS project (using either UIKit or SwiftUI), install the Adapty libraries using something like Swift Package Manager before you start working with the LLM. It will allow you to get into the code integration quicker if this step is already done. Here are some helpful additions to your prompts that should make working with the LLM that much easier: - If migrating from a native IAP implementation or something like RevenueCat: ``` Help me migrate from [existing implementation technique] to Adapty for handling in-app purchase subscriptions, including presenting Paywall Builder paywalls. Build a migration plan, but don’t implement it until you’ve shown me for review. ``` - If using something like SwiftUI: ``` This app uses SwiftUI for its interface, so use a native SwiftUI implementation for displaying and handling paywalls wherever applicable. ``` ## Use with Cursor **Cursor**, the AI code editor, makes it easier to integrate and maintain apps with Adapty's infrastructure. This guide shows you how to configure Cursor for better results when working with Adapty's SDK. ### Add Adapty documentation to Cursor Adding Adapty's documentation directly to Cursor means you don't have to switch between your code editor and browser. This setup gives you quick access to the latest implementation guides and best practices while coding. To add Adapty documentation to Cursor: 1. In Cursor, go to **Settings > Cursor settings**. Switch to **Indexing & Docs**. 2. Click **Add doc**. 3. Enter the URL `https://adapty.io/docs`. 4. Name the documentation `Adapty Docs` and click **Confirm** to save it. ### Add Adapty SDKs to Cursor For platform-specific development, we recommend adding the GitHub repositories for the Adapty SDKs you're using. This gives you access to the latest code examples, changelogs, and implementation details. Follow the same process as above, but use the relevant SDK repository URL: - **iOS SDK**: `https://github.com/adaptyteam/AdaptySDK-iOS` - **Android SDK**: `https://github.com/adaptyteam/AdaptySDK-Android` - **Flutter SDK**: `https://github.com/adaptyteam/AdaptySDK-Flutter` - **React Native SDK**: `https://github.com/adaptyteam/AdaptySDK-React-Native` - **Unity SDK**: `https://github.com/adaptyteam/AdaptySDK-Unity` Name each doc `Adapty SDK` (e.g., `Adapty Flutter SDK`). ## Reference Adapty in your prompts Once configured, you can reference Adapty documentation and SDKs using the `@` symbol in your prompts. This makes sure Cursor uses current and accurate information when generating code. **Example prompts:** - `@Adapty Docs help me install the Adapty SDK` - `Using @Adapty Flutter SDK, show me how to handle purchase restoration with proper error handling` - `@Adapty Docs @Adapty iOS SDK help me show a paywall created in the paywall builder` --- # File: adapty-paywall-builder-legacy.md --- --- title: "Design paywalls with legacy Paywall Builder" description: "Use the legacy Adapty Paywall Builder to manage in-app subscriptions." --- Paywall Builder is a simple no-code tool that lets you create custom paywalls — in-app storefronts in your mobile app where users can make purchases. It doesn't require any technical or design skills. You can easily improve the appearance of your paywalls, change the messages they display, and add buttons as needed. Plus, you can make changes to these screens in real-time while your app is live, without releasing a new mobile app version. :::warning The information below refers to the legacy Paywall Builder. For the new Paywall Builder, see [New Paywall Builder](/docs/adapty-paywall-builder). ::: Whether you're aiming to boost sales, highlight content, or offer exclusive features, the Paywall Builder offers an easy way to achieve your goals. To use the Adapty Paywall Builder: 1. Open the [**Products and Paywalls**](https://app.adapty.io/paywalls) section in the Adapty main menu and click the **Paywall** tab to open it. 2. Switch on the **Builder** toggle in the [**Paywalls/ Your paywall**](https://app.adapty.io/paywalls/create) window. 3. You'll find a selection of paywall templates designed by professionals, ready for you to pick the one that best suits your needs. From there, you can make minor adjustments to tailor it exactly to your preferences. By leveraging the Adapty Paywall Builder, you'll be able to create persuasive paywalls that seamlessly align with your app's branding and purpose. For more details on customizing templates, layout, products, text, and buttons, as well as localizing them, refer to the following sections: 1. [Paywall Builder templates](paywall-builder-templates) 2. [Paywall layout and products](paywall-layout-and-products) 3. [Paywall texts and buttons](paywall-texts-and-buttons) 4. [Paywall fonts](using-custom-fonts-in-paywall-builder) 5. [Paywall custom tags](custom-tags-in-paywall-builder) 6. [Paywall Builder localization](add-paywall-locale-in-adapty-paywall-builder) After you configure your paywalls and [add them to placements](add-audience-paywall-ab-test), you can show them in your mobile app. --- # File: adapty-paywall-builder.md --- --- title: "Design paywalls with new Paywall Builder" description: "Master Adapty's Paywall Builder to create high-converting in-app subscription offers." --- We're excited to introduce our **New Paywall Builder**, compatible with Adapty SDK v3.0 and later! This advanced no-code tool makes creating custom paywalls more intuitive and powerful than ever. You can craft beautiful, engaging paywalls with ease—no technical or design expertise required! ## Key Features of the New Paywall Builder - **Expanded Template Selection**: Choose from a wide array of professionally designed templates to kickstart your paywall creation. These templates offer various styles and layouts to meet different needs and preferences. - **Enhanced Flexibility**: Enjoy greater flexibility with the ability to use design layers and new elements like carousels, cards, product lists, and footers. These enhancements give you the creative freedom to build any type of paywall you envision. - **Revamped Existing Elements**: Existing elements have been significantly improved, offering more options and capabilities to bring your paywall ideas to life. - **Scalability**: If you manage multiple apps, [migrate](migrate-paywalls.md) your visual paywall configuration in one click instead of building from scratch. :::warning This section describes the new Paywall Builder, which works with iOS, Android, and React Native SDKs version 3.0 or higher and Flutter and Unity SDKs version 3.3.0 or higher. For information on the legacy Paywall Builder compatible with Adapty SDK v2.x or earlier, see [Design paywalls with legacy Paywall Builder](adapty-paywall-builder-legacy). ::: ## Structure of a paywall In the new Adapty Paywall Builder, a paywall is composed of the following: - [**Layout**](https://docs.adapty.io/v3.0/docs/paywall-layout-and-products): This is the foundational layer of the paywall, setting the background color and defining how products are displayed and top buttons positioned. - [**Hero Image**](paywall-head-picture): The main picture of the paywall. - **Main Area**: Here, you can place various elements like a product block, carousels, images, cards, texts, buttons, and lists. - **Footer**: Similar to the main area, but it's a container that always sticks to the bottom of the paywall on top of the main area. You can add as many elements as needed, and they will be arranged from top to bottom inside the footer in the same order shown in the left pane. - [**Elements**](adapty-paywall-builder#paywall-elements): The building blocks placed in the main area or footer to create your paywall. They are stacked in the order they appear in the left pane, from top to bottom. You can nest elements within each other, combine them into cards, or display them in a carousel. ## How to start designing a paywall with new Paywall Builder :::warning The new Paywall Builder is available for iOS and Android only and requires Adapty SDK v3.0 or later. Please make sure you've [iOS](migration-to-ios-sdk-v3.md), [Android](migration-to-android-sdk-v3.md), [Flutter](migration-to-flutter-sdk-v3.md), [React Native](migration-to-react-native-sdk-v3.md), and [Unity](migration-to-unity-sdk-v3.md) migration guides for your new paywalls to function properly! ::: To use the Adapty Paywall Builder: 1. Open the [**Paywalls**](https://app.adapty.io/paywalls) section in the Adapty main menu. 2. Open an existing paywall or [create a new one](create-paywall.md). 3. Go to the **Builder & Generator** tab. 4. Add products to the paywall if you haven't yet. Otherwise, click **Build no-code paywall**. 5. Select a template and click **Choose** or [migrate a paywall](migrate-paywalls.md) from another app. 6. Click **Choose** to confirm your choice. 7. [Add](#paywall-elements) and [customize](#customization-options) paywall elements. :::important For the paywall to be displayed, you must switch on the **Show on device** toggle in the Paywall Builder. ::: ## Migrate your legacy paywalls Currently, two versions of the Paywall Builder work in parallel in Adapty: - **New version**: Located in the **Builder & Generator** tab of the Paywall functionality in the Adapty Dashboard. This is the most recent and flexible version that provides many design features to build your perfect paywalls. :::note Paywalls designed with this version require Adapty SDK v3.0 or later. ::: - **Legacy version**: Located in the **Legacy Builder** tab of the Paywall functionality in the Adapty Dashboard. This version is outdated and should only be used to support app versions with installed SDK below v.3.х.х. We don't recommend using it for new paywalls as it will be deprecated soon. The migration of a paywall from the legacy Paywall Builder to the new one means that a new version of your paywall will be created in the **Builder & Generator** tab. This version can be edited with the new Paywall Builder and will be displayed in apps with installed Adapty SDK v3.0 or later. See [iOS](migration-to-ios-sdk-v3.md), [Android](migration-to-android-sdk-v3.md), [Flutter](migration-to-flutter-sdk-v3.md), [React Native](migration-to-react-native-sdk-v3.md), and [Unity](migration-to-unity-sdk-v3.md) migration guides for detailed reference on upgrading the Adapty SDK. The existing version of your paywall will stay in the Legacy Builder tab. You can continue adjusting it with the legacy Paywall Builder and it will be displayed in apps with installed Adapty SDK version 2.x or earlier. You will have paywalls in both Paywall Builder formats in parallel and separately until you need them. Changes made to the paywall configuration for one version will not affect the configuration built for another one. To migrate a paywall to the new Paywall Builder: 1. Open the paywall you want to migrate. 2. Open the **Builder & Generator** tab. 3. Click **Migrate paywall**. 4. After the migration is done, review the result and make sure the paywall looks as it should. If not, correct it. 5. Click the **Save** button. 6. If there are any issues, they will be highlighted in red and you will see them immediately. Fix them and save the paywall again. You can migrate your paywalls one by one so you can review and fix them if necessary. ## Paywall elements The elements you add to your paywall appear in the left pane of the Paywall window. Their order in this pane reflects their order on the paywall. **Paywall elements** in Adapty are categorized as simple or compound: - **Simple Elements**: These are individual items that cannot contain other elements. Examples include text, images, and buttons. - **Compound Elements**: These can contain other elements or have their own structure. Examples include: - [Product lists](paywall-product-block) with products - [Carousels](paywall-carousel) with child elements - [Cards](paywall-card) with child elements - Lists with list items - Link blocks with links inside **Enhancements** you can add include: 1. [Predefined tag variables for product info](paywall-builder-tag-variables) 2. [Custom tags](custom-tags-in-paywall-builder) 3. [Custom fonts](using-custom-fonts-in-paywall-builder) 4. [Localization](add-paywall-locale-in-adapty-paywall-builder) Once configured, you can [add paywalls to placements](add-audience-paywall-ab-test) to display them in your mobile app. For more details on displaying paywalls, see the platform-specific articles: - [iOS](ios-quickstart-paywalls.md) - [Android](android-quickstart-paywalls.md) - [Flutter](flutter-quickstart-paywalls.md) - [React Native](react-native-quickstart-paywalls.md) - [Unity](unity-quickstart-paywalls.md) ## Customization Options You can set up each element flexibly: - **Style** tab: Adjust the element's size, appearance, background color or image, frame, and transparency. Additional options like page control and slideshow settings are available for certain elements, such as carousels. - **Layout** tab: Set the element's position and its child elements' positions using offset (moving an element without changing its size or the parent's size) or padding (moving the element with possible resizing of the parent to fit the child's size and position). - **Contents** tab: Configure the content of compound elements. ## We Value Your Feedback Your feedback is invaluable to us. If you encounter any issues or have suggestions for improvements, please reach out to us. We're here to support you and enhance your experience with the new Paywall Builder. 📧 **Contact Us**: [Adapty Support](mailto:support@adapty.io) Enjoy building with the new Paywall Builder, and take your monetization strategy to the next level with our enhanced tools and features! --- # File: add-audience-paywall-ab-test.md --- --- title: "Add audience and paywall or A/B test to placement" description: "Run A/B tests on paywalls for different audience segments in Adapty." --- **Audiences** in Adapty are groups of users defined by [segments](segments). They let you show paywalls, onboardings, and A/B tests to the users who should see them. Build segments with filters to ensure each group gets the right content. When you add an audience to a [placement](placements), you target paywalls, onboardings, or A/B tests at a specific user group. Linking an audience to a placement makes sure the right users see the right content at the right moment in their app journey. Open the placement where you want to add a paywall, onboarding, or A/B test, or create a new one in the [Placements](https://app.adapty.io/placements) menu. :::note To proceed, ensure that you created a paywall, onboarding, or A/B test you want to run and an audience, you'd like to specify. ::: 1. In the **Placements/ Your placement** window, add a paywall, onboarding, or A/B test to display for default *All users* audience. To do this, click either the **Run paywall** or **Run A/B test** button, then select the desired paywall, onboarding, or A/B test from the dropdown list. 2. If you want to use more than one audience in the placement to create personalized paywalls tailored to different user groups, click the **Add audience** button and choose the desired user segment from the list. 3. Now add the paywall, onboarding, or A/B test to show for this audience. 4. Add as many audiences as you need. 5. If you have more than one audience, check that the audiences have the correct priorities. 6. Click the **Save and publish button**. --- # File: add-offer-to-paywall.md --- --- title: "Add offer to paywall" description: "Add custom offers to paywalls in Adapty to boost conversion rates." --- Offers in the App Store and Google Play are special deals or discounts provided by these platforms for in-app purchases. To make an offer visible and selectable within a [paywall](paywalls) for your app's users, follow these steps: While [configuring the products on a paywall](create-paywall), choose an offer you [created earlier](create-offer) for this product from the **Offer** list. The list is available only for the products that have offers. :::info Paywalls created with the Adapty Paywall Builder will display only the first phase of a [multi-phase Google subscription offer](https://support.google.com/googleplay/android-developer/answer/12154973). However, rest assured that when a user purchases the product, all offer phases will be applied as configured in Google Play. :::

--- # File: add-paywall-locale-in-adapty-paywall-builder.md --- --- title: "Add paywall locale in Adapty Paywall Builder" description: "Add localized paywalls in Adapty’s Paywall Builder to improve user experience worldwide." --- Localizing is a tedious process that requires time and precision. When using Paywall Builder, Adapty does almost all of the work for you, as most of the things you'll need work out of the box. This page describes how it works. Suppose you've finished configuring your paywall in the default `en` localization and you like the result. Now it's time to add another language. ## Add and set up localization 1. Click **Add locale** and select all the languages you want to include in your app. 2. Open the **Localization** menu to view all added locales. New locales will be pre-filled with values from the default language. Now, you can translate the content manually, use AI, or export the localization file for external translators. ## Translating paywalls with AI AI-powered translation is a quick and efficient way to localize your paywall. :::note To use AI for paywall translation, you’ll need a Pro+ or Enterprise plan. ::: We automatically detect which lines have never been translated or have changed in English since their last translation and mark them as needing an update. Lines that were already translated and haven't changed will keep their original translation and won’t be re-translated. Rich text formatting (bold, italic, colored text, etc.) won’t be preserved in the translated version. Please adjust the translated text manually as needed. 1. Select the languages for translation. 2. Click **AI Translate** to apply translations. The paywall lines will be translated and added to the table. ## Exporting localization files for external translation You can export localization files to share with your translators and then import the translated results back into Adapty. Exporting by the **Export** button creates individual `.csv` files for each language, bundled into a single archive. If you only need one file, you can export it directly from the language-specific menu. Once you’ve received the translated files, use the **Import** button to upload them all at once or individually. Adapty will automatically validate the files to ensure they match the correct format and paywall configuration structure. ### Import file format To ensure a successful import, the import file must meet the following requirements: - **File Name and Extension:** The file name must match the locale it represents and have a `.csv` extension. You can verify and copy the locale name in the Adapty Dashboard. If the name is not recognized, the import will fail. - **Valid CSV:** The file must be a valid CSV format. Invalid files will fail to import - **Only Commas as Separators**: Use commas as separators. Other separators will result in errors. - **Header Line**: The file must include a header line. - **Correct Column Names:** The column names must be **id** and **value**. - **No Additional Entities:** Ensure the file doesn’t include entities not present in the current paywall configuration. Extra entities will result in errors. - **Partial import:** The file can include all or just some entities from the current paywall configuration. | **Issue** | **Solution** | | ---------------------------------------------- | ------------------------------------------------------------ | | **Imported .csv files are invalid** | Validate the file to ensure it adheres to CSV standards. Check for missing or extra commas, incorrect separators, missing header lines, and ensure the column names are **id** and **value**. | | **Some of the languages are not in the table** | Ensure file names match the locale names exactly as shown in the localization table. If they don’t match, rename them accordingly. Also, verify the file’s content to ensure it relates to the paywall configuration. | ## Manual localization Sometimes, you might want to tweak translations, add different images for specific locales, or even adjust remote configurations directly. 1. Choose the element you want to translate and type in a new value. You can update both **String** and **List** values or replace images with those better suited for the locale. 2. Take advantage of the context menu in the English locale to resolve localization issues efficiently: - **Copy this value to all locales**: Overwrites any changes made in non-English locales for the selected row, replacing them with the value from the English locale. - **Revert all row changes to original values**: Discards any changes made during the current session and restores the values to their last saved state. After adding locales to a paywall, make sure to implement locale codes correctly in your app's code. See [iOS](localizations-and-locale-codes.md), [Android](android-localizations-and-locale-codes.md), [Flutter](flutter-localizations-and-locale-codes.md), [React Native](react-native-localizations-and-locale-codes.md), and [Unity](unity-localizations-and-locale-codes.md). ### Preview the localization result You can check your texts while editing by simply switching over back to the **Builder & Generator** tab and selecting another locale: :::note Pay attention to the locale code (`en`, `fr` and `it` ). You'll need to pass it to the `getViewConfiguration` method of our SDK to get the correct localization. You can learn more about it in [iOS](get-pb-paywalls.md), [Android](android-get-pb-paywalls.md), [Flutter](flutter-get-pb-paywalls.md), [React Native](react-native-get-pb-paywalls.md), and [Unity](unity-get-pb-paywalls.md). ::: Once you add locales to a paywall, see [iOS](localizations-and-locale-codes.md), [Android](android-localizations-and-locale-codes.md) for more information. --- # File: add-product-to-paywall.md --- --- title: "Add product to paywall" description: "Learn how to add and manage products on paywalls in Adapty." --- To make a product visible and selectable within a [paywall](paywalls) for your app's users, follow these steps: 1. While [configuring a paywall](create-paywall), click the **Add product** button under the **Products** title. 2. From the opened drop-down list, select the products that will be shown to your customers. The list contains only previously created products. The order of the products is preserved on the SDK side, so it's important to consider the desired order when configuring the paywall. Additionally, you can specify an offer for a product if desired. 3. Click the **Save as draft** or **Save and publish** button depending on the status of the paywall. Please keep in mind that after creation, it is not recommended to edit, add, or delete products to the paywall as this may affect the paywall metrics. --- # File: add-remote-config-locale.md --- --- title: "Remote config paywall localization" description: "Add remote config locales to personalize Adapty paywalls." --- Adapting your paywalls for different languages is essential in a world with diverse cultures. Localization allows you to create tailored experiences for users in specific regions. For each paywall, you can add versions in various languages, ensuring that your product resonates with local audiences. If you’ve [designed a paywall using remote config](customize-paywall-with-remote-config), you can use the same remote config to set up localizations. Whether in table view or JSON format, you can easily adjust settings for each language. For example, translate string keys, toggle Boolean values (e.g., `TRUE` for English, `FALSE` for Italian), or even swap out background images. With this flexibility, you keep your core setup while tailoring the user experience for different languages. ## Setting up localization for remote configured paywalls 1. Go to the [**Paywalls**](https://app.adapty.io/paywalls) section in Adapty. 2. Click the paywall to open it. 3. Go to the **Remote config** tab. 4. Click **Locales** and select the languages you want to support. Save your changes to add these locales to the paywall. Now, you can translate the content manually, use AI, or export the localization file for external translators. ## Translating paywalls with AI AI-powered translation is a quick and efficient way to localize your paywall. :::note To use AI for paywall translation, you’ll need a Pro, Pro+, or Enterprise plan. ::: You can translate both **String** and **List** values. By default, all lines are selected (highlighted in violet). Lines that have already been translated are marked in green and won’t be included in the new translation by default. Lines that are not selected or translated appear in gray. 1. Select the lines to translate. It's a good idea to uncheck lines with IDs, URLs, and variables to prevent AI from translating them. 2. Select the languages for translation. 3. Click **AI Translate** to apply translations. The selected lines will be translated and added to the paywall, with the translated lines marked green. ## Exporting localization files for external translation While AI-powered localization is becoming a popular trend, you might prefer a more reliable method, like using professional human translators or a translation agency with a strong track record. If that’s the case, you can export localization files to share with your translators and then import the translated results back into Adapty. Exporting by the **Export** button creates individual `.json` files for each language, bundled into a single archive. If you only need one file, you can export it directly from the language-specific menu. Once you’ve received the translated files, use the **Import** button to upload them all at once or individually. Adapty will automatically validate the files to ensure they match the correct format. ### Import file format To ensure a successful import, the import file must meet the following requirements: - **File Name and Extension:** The file name must match the locale it represents and have a `.json` extension. You can verify and copy the locale name in the Adapty Dashboard. If the name is not recognized, the import will fail. - **Valid JSON:** The file must be a valid JSON. If it is not, the import will fail. ## Manual localization Sometimes, you might want to tweak translations, add different images for specific locales, or even adjust remote configurations directly. 1. Choose the element you want to translate and type in a new value. You can update both **String** and **List** values or replace images with those better suited for the locale. 2. Take advantage of the context menu in the English locale to resolve localization issues efficiently: - **Copy this value to all locales**: Overwrites any changes made in non-English locales for the selected row, replacing them with the value from the English locale. - **Revert all row changes to original values**: Discards any changes made during the current session and restores the values to their last saved state. After adding locales to a paywall, make sure to implement locale codes correctly in your app's code. See [iOS](localizations-and-locale-codes.md), [Android](android-localizations-and-locale-codes.md) --- # File: adjust.md --- --- title: "Adjust" description: "Connect Adjust with Adapty for better subscription tracking and analytics." --- [Adjust](https://www.adjust.com/) is one of the leading Mobile Measurement Partner (MMP) platforms, that collects and presents data from marketing campaigns. This helps companies track their campaign performance. Adapty provides a complete set of data that lets you track [subscription events](events) from stores in one place. With Adapty, you can easily see how your subscribers are behaving, learn what they like, and use that information to communicate with them in a way that's targeted and effective. Therefore, this integration allows you to track subscription events in Adjust and analyze precisely how much revenue your campaigns generate. The integration between Adapty and Adjust works in two main ways. 1. **Receiving attribution data from Adjust** Once you've set up the Adjust integration, Adapty will start receiving attribution data from Adjust. You can easily access and view this data on the user's profile page. 2. **Sending subscription events to Adjust** Adapty can send all subscription events which are configured in your integration to Adjust. As a result, you'll be able to track these events within the Adjust dashboard. This integration is beneficial for evaluating the effectiveness of your advertising campaigns. ## How to set up Adjust integration To setup the integration with Adjust go to [Integrations > Adjust](https://app.adapty.io/integrations/adjust) in the Adapty Dashboard, turn on a toggle from off to on, and fill out fields. The next step of the integration is to set credentials. 1. If you have enabled OAuth authorization on the Adjust platform, it is mandatory to provide an **OAuth Token** during the integration process for your iOS and Android apps. 2. Next, you need to provide the **app tokens** for your iOS and Android apps. Open your Adjust dashboard and you'll see your apps. :::note You may have different Adjust applications for iOS and Android, so in Adapty you have two independent sections for that. If you have only one Adjust app, just fill in the same information ::: You will need to copy **App Token** and paste it to Adapty. Another important thing is that Adjust doesn't support events older than 58 days. So, if you have an event that is more than 58 days old, Adapty will send it to Adjust, but the event datetime will be replaced by the current timestamp. ## Events and tags Adjust works a bit differently from other platforms. You need to manually create events in Adjust dashboard, get event tokens, and copy-paste them to appropriate events in Adapty. So first step here is to find event tokens for all events that you want Adapty to send. To do that go to All Settings in your Adjust dashboard. Copy the event token and paste it to Adapty. Below the credentials, there are three groups of events you can send to Adjust from Adapty. Check the full list of the events offered by Adapty [here](events). Adapty will send subscription events to Adjust using a server-to-server integration, allowing you to view all subscription events in your Adjust dashboard and link them to your acquisition campaigns. ## SDK configuration For Adapty to send subscription data to Adjust, pass the Adjust device ID using the `setIntegrationIdentifier()` SDK method. For Adjust version 5.0 or later, use the following example: ```swift showLineNumbers class AdjustModuleImplementation { 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") } } ``` ```kotlin showLineNumbers 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 -> // handle the error } } ``` ```java showLineNumbers Adjust.getAdid(adid -> { if (adid == null) return; Adapty.setIntegrationIdentifier("adjust_device_id", adid, error -> { if (error != null) { // handle the error } }); }); Adjust.getAttribution(attribution -> { if (attribution == null) return; Adapty.updateAttribution(attribution, "adjust", error -> { // handle the error }); }); ``` ```javascript showLineNumbers 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!; await Adapty().updateAttribution(attribution, source: "adjust"); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` ```typescript showLineNumbers var adjustConfig = new AdjustConfig(appToken, environment); // Before submiting Adjust config... adjustConfig.setAttributionCallbackListener(attribution => { // Make sure Adapty SDK is activated at this point // You may want to lock this thread awaiting of `activate` adapty.updateAttribution(attribution, "adjust"); }); // ... Adjust.create(adjustConfig); Adjust.getAdid((adid) => { if (adid) adapty.setIntegrationIdentifier("adjust_device_id", adid); }); ``` --- # File: airbridge.md --- --- title: "Airbridge" description: "Connect Adapty with Airbridge to track marketing and attribution insights." --- [Airbridge](https://www.airbridge.io/) offers an integrated marketing performance analysis for websites and mobile apps by consolidating data collected from multiple devices, platforms, and channels. Using Airbridge's Identity Resolution Engine, you can combine scattered customer identity data from web and app interactions into a unified people-based identity, resulting in more accurate attribution. Adapty provides a complete set of data that lets you track [subscription events](events) from stores in one place. With Adapty, you can easily see how your subscribers are behaving, learn what they like, and use that information to communicate with them in a way that's targeted and effective. The integration between Adapty and Airbridge operates in two main ways. 1. **Receiving attribution data from Airbridge** Once you've set up the Airbridge integration, Adapty will start receiving attribution data from Airbridge. You can easily access and view this data on the user's page. 2. **Sending subscription events to Airbridge** Adapty can send all subscription events which are configured in your integration to Airbridge. As a result, you'll be able to track these events within the Airbridge dashboard. This integration is beneficial for evaluating the effectiveness of your advertising campaigns. ## How to set up Airbridge integration To integrate Airbridge go to [Integrations > Airbridge](https://app.adapty.io/integrations/airbridge), turn on a toggle from off to on, and fill out fields. First of all set credentials to build a connection between your Airbridge and Adapty profiles. Airbridge app name and Airbridge API token are required. They both can be found in your Airbridge dashboard in the [Third-party Integrations > Adapty](https://app.airbridge.io/app/testad/integrations/third-party/adapty) section. Adapty API token field is pre-generated on the Adapty backend. You will need to copy the value of Adapty API token and paste it into the Airbridge Dashboard in the Adapty Authorization Token field. ## Events and tags Below the credentials, there are three groups of events you can send to Airbridge from Adapty Simply turn on the ones you need. When subscription-related events happen, Adapty sends events to Airbridge. After receiving them Airbridge sends attribution result information to Adapty. The historical events will be sent in the last 24 hours instead of the real event time ## SDK configuration For the integration, you should pass `airbridge_device_id` to profile builder and call `setIntegrationIdentifier` as it is shown in the example below: ```swift showLineNumbers do { try await Adapty.setIntegrationIdentifier( key: "airbridge_device_id", value: AirBridge.deviceUUID() ) } catch { // handle the error } ``` ```kotlin showLineNumbers Airbridge.getDeviceInfo().getUUID(object: AirbridgeCallback.SimpleCallback() { override fun onSuccess(result: String) { Adapty.setIntegrationIdentifier("airbridge_device_id", result) { error -> if (error != null) { // handle the error } } } override fun onFailure(throwable: Throwable) { } }) ``` ```javascript showLineNumbers final deviceUUID = await Airbridge.state.deviceUUID; try { await Adapty().setIntegrationIdentifier( key: "airbridge_device_id", value: deviceUUID, ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` ```typescript showLineNumbers try { const deviceId = await Airbridge.state.deviceUUID(); await adapty.setIntegrationIdentifier("airbridge_device_id", deviceId); } catch (error) { // handle `AdaptyError` } ``` Read more about airbridgeDeviceId in [Airbridge documentation.](https://developers.airbridge.io/v1.1-en/docs/airbridge-device-id) --- # File: amplitude.md --- --- title: "Amplitude" description: "Integrate Amplitude with Adapty for better user behavior insights." --- [Amplitude](https://amplitude.com/) is a powerful mobile analytics service. With Adapty, you can easily send events to Amplitude, see how users behave, and then make smart decisions. Adapty provides a complete set of data that lets you track [subscription events](events) from stores in one place and sends it to your Amplitude account. This allows you to match your user behavior with their payment history in Amplitude, and inform your product decisions. ### How to set up Amplitude integration Within Adapty, you can set up separate flows for **production** and **test events** from the Apple or Stripe sandbox environment or Google test account. - For production events, enter the **Production** API keys from the Amplitude dashboard, with a unique API key for each platform: iOS, Android, and Stripe. - For test events, use the **Sandbox** fields as needed. To set up the Amplitude integration: 1. Open [**Integrations** -> **Amplitude**](https://app.adapty.io/integrations/amplitude) in your Adapty Dashboard. 2. Toggle on **Amplitude integration** to enable it. 3. Fill in the integration fields: | Field | Description | | ------------------------------------------ | ------------------------------------------------------------ | | **Amplitude iOS/ Android/ Stripe API key** | Enter the Amplitude **API Key** for iOS/ Android/ Stripe into Adapty. Locate it under **Project settings** in Amplitude. For help, check [Amplitude docs](https://amplitude.com/docs/apis/authentication). Start with **Sandbox** keys for testing, then switch to **Production** keys after successful tests. | 4. Optional settings for further customization: | Parameter | Description | | --------------------------------------- | ------------------------------------------------------------ | | **How the revenue data should be sent** | Choose whether to send gross revenue or revenue after taxes and commissions. See [Store commission and taxes](controls-filters-grouping-compare-proceeds#store-commission-and-taxes) for details. | | **Exclude historical events** | Choose to exclude events before Adapty SDK installation, preventing duplicate data. For example, if a user subscribed on January 10th but installed the Adapty SDK on March 6th, Adapty will only send events from March 6th onward. | | **Send User Attributes** | Select this option to send user-specific attributes like language preferences. | | **Always populate user_id** | Adapty automatically sends `device_id` as `amplitudeDeviceId`. For `user_id`, this setting defines behavior:
  • **ON**: Sends Adapty `profile_id` if `amplitudeUserId` or `customer_user_id` aren’t available.
  • **OFF**: Leaves `user_id` empty if neither ID is available.
| 5. Choose the events you want to receive and [map their names](amplitude#events-and-tags). 6. Click **Save** to confirm your changes. Once you click **Save**, Adapty will start sending events to Amplitude. In addition to events, Adapty sends [subscription status](subscription-status) and the subscription product ID to [Amplitude user properties](https://help.amplitude.com/hc/en-us/articles/115002380567#h_39e46c92-7b7f-4358-a96f-c82cc3342e3e). ### Events and tags Below the credentials, there are three groups of events you can send to Amplitude from Adapty. Simply turn on the ones you need. Check the full list of the events offered by Adapty [here](events). We recommend using the default event names provided by Adapty. But you can change the event names based on your needs. Adapty will send subscription events to Amplitude using a server-to-server integration, allowing you to view all subscription events in your Amplitude dashboard. ### SDK configuration Use the `setIntegrationIdentifier()` method to set the `amplitude_device_id` parameter. It's a must to set up the integration. If you have a user registration, you can pass `amplitude_user_id` as well. **Setting amplitudeDeviceId** ```swift showLineNumbers do { try await Adapty.setIntegrationIdentifier( key: "amplitude_device_id", value: Amplitude.instance().deviceId ) } catch { // handle the error } ``` **Setting amplitudeUserId** ```swift showLineNumbers do { try await Adapty.setIntegrationIdentifier( key: "amplitude_user_id", value: "YOUR_AMPLITUDE_USER_ID" ) } catch { // handle the error } ``` **Setting amplitudeDeviceId** ```kotlin 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 // Adapty.setIntegrationIdentifier("amplitude_device_id", amplitudeDeviceId) { error -> if (error != null) { // handle the error } } ``` **Setting amplitudeUserId** ```kotlin 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 amplitudeUserId = amplitude.store.userId // Adapty.setIntegrationIdentifier("amplitude_user_id", amplitudeUserId) { error -> if (error != null) { // handle the error } } ``` **Setting amplitudeDeviceId** ```javascript showLineNumbers final Amplitude amplitude = Amplitude.getInstance(instanceName: "YOUR_INSTANCE_NAME"); try { await Adapty().setIntegrationIdentifier( key: "amplitude_device_id", value: amplitude.getDeviceId(), ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` **Setting amplitudeUserId** ```javascript showLineNumbers final Amplitude amplitude = Amplitude.getInstance(instanceName: "YOUR_INSTANCE_NAME"); try { await Adapty().setIntegrationIdentifier( key: "amplitude_user_id", value: "YOUR_AMPLITUDE_USER_ID", ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` **Setting amplitudeDeviceId** ```csharp showLineNumbers using AdaptySDK; Adapty.SetIntegrationIdentifier( "amplitude_device_id", amplitude.getDeviceId(), (error) => { // handle the error }); ``` **Setting amplitudeUserId** ```csharp showLineNumbers using AdaptySDK; Adapty.SetIntegrationIdentifier( "amplitude_user_id", "YOUR_AMPLITUDE_USER_ID", (error) => { // handle the error }); ``` **Setting amplitudeDeviceId** ```typescript showLineNumbers try { await adapty.setIntegrationIdentifier("amplitude_device_id", deviceId); } catch (error) { // handle `AdaptyError` } ``` **Setting amplitudeUserId** ```typescript showLineNumbers try { await adapty.setIntegrationIdentifier("amplitude_user_id", userId); } catch (error) { // handle `AdaptyError` } ``` --- # File: analytics-charts.md --- --- title: "Charts" description: "Analyze subscription trends with Adapty’s analytics charts." --- Install Adapty SDK and start analyzing real-time metrics of your iOS or Android app with advanced filters and grouping, such as ad network, ad campaign, country, paywall, product, and more. ### Controls that make data informative For all the charts in Adapty, you can use a range of controls to get more data-based insights. #### 1. Time ranges and comparison. When you set a time range for a chart in the Adapty calendar, you have several quick options to choose from. - Last 7 days - Last month - from the current date to the same day in the previous month - Last 28 days - useful for tracking weekly subscription products - Last 3 months - Last 6 months - Last year - Previous month - the full calendar month before the current - This month - from the 1st day of the current month till today - This quarter - from the 1st day of the current quarter till today - This year - from the 1st day of the current year till today Also, you can select the Custom option to set any period you need. We have 2 formats of date and time - American and European. You can set one of them in your Adapty account as described [here.](account) To evaluate the dynamic of your app's metrics we recommend using the comparison located next to the calendar. The easiest way to use it is just by choosing to compare to the previous period. Though you may also customize your range of comparison according to your needs. How can one read the comparison insights? 1. When you choose the comparison period you can switch an on/off toggle to show the comparison on the chart or only as a number. 2. We show the difference between your current result and the result of the previous period as a green or red number for the higher and lower results accordingly. 3. If you have no grouping or only one grouping selected the comparison will be also shown on the chart. You can choose the type of chart - area, line, column - to make the visual difference more obvious. 4. Hover on the chart and take a look at the details in an appearing tooltip. 5. You can see both multiple grouping and comparison at the same time on one chart for a specific type of chart - columns. #### 2. Filters and grouping. ### Metrics Advanced Analytics allows you to track the following metrics. #### Revenue Total money received from both subscriptions and one-time purchases. Does not include revenue from subscriptions and purchases that were refunded afterward. #### Monthly Recurring Revenue (MRR) The measure of the predictable and recurring revenue components of your subscription business. Calculated as following: Where: Ps - subscription price Ns - number of active paid subscriptions for this subscription Dsm - subscription duration in months (0.25 for weekly subscriptions) Basically, MRR shows revenue from all active subscriptions normalized to one month. For example, for a yearly subscription, instead of counting full revenue from the start, revenue is split into 12 equal parts which are evenly spread across 12 month period. E.g. if there are 2 active yearly subscriptions with price $240 and 10 monthly subscriptions with price $30, MMR = 240 _ 2 / 12 + 30 _ 10 / 1 = $340 #### ARPU ARPU chart displays the average revenue per user (ARPU) for your app. ARPU is calculated as revenue for a given period / users (non-unique installs) for the same period. For example, if your app generates $10,000 in revenue in a day and has 1,000 new users on that day, the ARPU for that day would be $10 ($10,000/1,000). The ARPU metric provides insight into the financial performance of your app and can be used to identify trends and track changes in revenue over time. #### ARPPU An average revenue per paid user. Calculated as `revenue / number of users who paid` If revenue this day is $1000, and there were 50 users who made at least one purchase, ARPPU = 1000 / 50 = $20. #### Installs The Installs chart shows the total number of users who have installed the app for the first time, as well as any reinstalls by existing users. This includes multiple installations by the same user on different devices. Please note that incomplete downloads or installations that are canceled before completion are not counted toward the install count. Please note that If your app has in-app user authentication, the Installs chart may also include the count of new logged-in users who have accessed your app multiple times. For more details, see [iOS](identifying-users.md), [Android](android-identifying-users.md), [Flutter](flutter-identifying-users.md), [React Native](react-native-identifying-users.md), and [Unity](unity-identifying-users.md). ### Subscriptions #### Active Subscriptions The number of currently active subscriptions. #### Subscriptions by renewal status The number of active subscriptions segmented by renewal status, which indicates whether or not this subscription will be renewed in the next period. #### Cancelled Subscriptions The number of subscriptions auto-renew status of which was switched off (canceled by user). When the auto-renew status for the subscription is off, this means that this subscription won't be automatically renewed to the next period, yet the user still has premium features until the end of the current period. ![](https://files.readme.io/557ad8b-cancelledsubscrs.JPG "cancelledsubscrs.JPG") #### Churned (Expired) Subscriptions The number of subscriptions that expired, i.e. the user has no access to the premium features of the app, in contrast to the canceled subscriptions. Usually, it means that the user has decided to stop paying for the app or encountered a billing issue. ![](https://files.readme.io/10b4632-churned.JPG "churned.JPG") #### Reactivated Subscriptions The number of subscriptions auto-renew status of which was switched on (reactivated by user). When the auto-renew status for the subscription is on, this means that this subscription will be automatically renewed to the next period in the future. ![](https://files.readme.io/b29c338-reactivated.JPG "reactivated.JPG") #### Non-subscriptions The Non-subscriptions chart displays the number of in-app purchases such as consumables, non-consumables, and non-renewing subscriptions. The chart doesn't include renewable payments. The chart shows the total count of these types of in-app purchases and can help you track user behavior and engagement over time. For example, if your app records 500 one-time purchases and 1,000 consumable purchases in a week, the Non-subscriptions chart will display a total count of 1,500 purchases for the week. Please note that this chart does not display revenue information. ### Trials #### Active Trials The number of trials that are currently active. #### New Trials The number of trials activated. #### Trials Renewal Cancelled The number of trials with cancelled renewal (cancelled by user). When the renewal for the trial is disabled, this means that this trial won't be automatically converted to paid subscription, yet the user still has premium features of the app until the end of the current period. #### Expired (Churned) Trials The number of trials that expired, i.e. the user has no access to the premium features of the app. Usually, it means that the user has decided not to pay for the app or encountered a billing issue. ### Issues #### Refund Events The number of refunded purchases and subscriptions. #### Refund Money Amount of money that was refunded. Calculated before store's fee. ### Filters Advanced Analytics provides the following filters. #### Countries Filter metrics by user's store country (if available, otherwise IP country is used). #### Products Filter metrics by product (both subscriptions and one-time purchases are available). #### Platforms Filter metrics by platform (iOS/Android). #### A/B tests Filter metrics by A/B tests that are associated with purchases. #### Attribution Filter metrics by Attribution fields like Status, Channel, Campaign, etc. --- # File: analytics-cohorts.md --- --- title: "Cohort analysis" description: "Use analytics cohorts in Adapty to track user engagement and subscription trends." --- Adapty cohorts are designed to answer several important questions: 1. On what day does a cohort pay off? 2. How much money does the app earn for a specific cohort? 3. How much money can I spend to attract a paying customer? 4. How long does it take to recoup the ad spend? Cohorts work with the app data we gather through SDK and store notifications and don't require any additional configuration from your side. ### Cohorts by renewals or by days You can analyze cohorts by renewals or by days. The control changes the headings of the columns. Consequently, the approach to analysis also changes. Tracking **by days** provides valuable insights for budgeting and understanding payment timelines. This is particularly useful for tracking non-subscription products, such as consumables or one-time purchases. In this mode, the blue color in the table cells tends to be concentrated in the middle of the lines due to two key factors. Firstly, viewing cohorts by days allows for early visibility of payments associated with short duration products, while in the renewals view, they are grouped with monthly and yearly renewals. Secondly, delayed payments contribute to the distribution pattern, as some users pay later than expected. Whereas tracking **by renewals** shows the retention and churn of the cohorts from one payment to another without consideration of the date. So late users who paid with any delay (it can be months) are added to the number of their subscription period. This approach doesn't reflect the situation of calendar earnings but is definitely more convenient to analyze the retention and churn of the cohorts and get insights from their behavior. Choose your convenient mode or use them both for more conclusions and ideas. ### How Adapty builds cohorts Let's see in the example of cohorts by renewals how the table is formed. To build cohorts, we use two measures: app installations and transactions (purchases). Every row of a cohort represents a specific time interval: from a day to a year. Each row starts with the number of users who installed the app during this interval and activated a subscription or made a lifetime/non-subscription product purchase. Every next column in the row shows the number of users who renewed a subscription to this period. M3 stands for month 3 and means that subscribers had 3 consecutive renewals to this point, W7 stands for week 7, and Y2 stands for year 2. Sometimes you can see P2 in cohorts. P stands for Period of subscription. Adapty displays instead of W/M/Y when there are multiple products with different renewal periods present in the same cohort. We use gradient colors to highlight differences in cohort values. The biggest numbers have more saturated colors. In the image below you can see a typical cohort. 1. This cohort displays the data only for weekly products (mark #1). 2. It doesn't exclude proceeds and shows the revenue as absolute values (mark #2). 3. The time period we're working with is the last 6 months, and every cohort segment is 1 month long (mark #3). 4. The **Total** row (mark #4) displays the cumulative value for each period. $442K in the first cell of the **Total** row accumulates the first period (subscription activation) revenue from all months (Nov, Dec, and so on) until the end of the timeframe. The Total cell shows the number of customers who installed the app during the whole period. 5. The first column of the Nov 2023 row (mark #5) shows the first period (subscription activation) revenue of $37.7K from the customers who installed the app in Nov 2023. The number of customers who installed the app in Nov 2023 which is 95,129, is shown in the header column. The second column of the Nov 2023 row shows week 2 (subscriptions renewed to the 2nd week) revenue of $8,77K who installed the app in Nov 2023. 6. On the table, you can see the Total revenue, ARPU, ARPPU, and ARPAS (mark #6). You can read more about them a little further in this article. 7. You can configure the columns in the right part of the table using the **Columns** dropdown field (mark #7). 8. Above the table on the right (mark #8), there is also a dropdown field to calculate stores' commission fees and tax calculations for the specific cohort analyses. You can learn about how Adapty calculates store commission fees and taxes further in [this article](controls-filters-grouping-compare-proceeds#store-commission-and-taxes). After choosing the corresponding option from the dropdown the revenue data will be recalculated based on it. 9. On the right side of the table, you can see predicted revenue (Predicted Revenue) and predicted lifetime value (Predicted LTV) (mark #9). The **Predicted Revenue** field estimates the total revenue generated by a subscriber cohort within a specific timeframe, while the **Predicted LTV** field represents the anticipated value of each user in the cohort. You can hover on any cell in the cohort to view detailed metrics for this period. The cells with oblique lines in the background are the periods that are not finished yet, so the values in them might be increased. ### Filters, metrics, cohort segments, and export in CSV Adapty offers a wide range of controls to help you gain valuable insights when looking into your cohorts' analyses. By default, Adapty builds cohorts based on the data from all purchases. It might be useful to filter all the products of the same duration or specific products. You can also use country, store, paywall, segment, and attribution data as a filter. You can find more information about the available controls, filters, grouping options, and how to use them in [this documentation.](controls-filters-grouping-compare-proceeds) On the right of the control panel, there's a button to export cohort data to CSV. You can then open it in Excel, or Google Sheets, or import it into your own analytical system. There are 4 metrics that can be shown in cohorts: Subscriptions, Payers, Revenue, ARPU, ARPPU, and ARPAS. You can either display them as absolute values or as a relative change from the start of the cohort. You can set the date range for cohorts and choose the segment. The segment determines a timespan for each row of the cohort. ### Subscriptions, payers, total revenue, ARPU, ARPPU and ARPAS **Subscriptions** are the total count of active subscriptions, lifetime purchases, and non-subscription purchases made by a cohort within a selected timeframe. Monitoring this metric helps you understand customer behavior and the effectiveness of your offerings. This insight allows you to refine your product strategy, tailor marketing efforts, and optimize revenue streams. **Payers** are the total number of users who made a purchase within a cohort. It helps you understand how many unique users contribute to your revenue. For apps with a significant amount of non-subscription purchases, this metric can highlight the true reach of your product offerings, showing whether a broad user base is making purchases or if revenue is driven by a smaller group of repeat buyers. Understanding the number of payers helps in assessing customer engagement, planning targeted marketing, and optimizing revenue strategies. **Total revenue** is accumulated for a cohort within a selected timeframe (Nov 25, 2022 — May 24, 2023). It helps you to understand how much money you collected from users from a specific cohort and calculate ROAS. For example, if the ad spend for September 2022 was $10000, and the total proceeds for September 2022 cohort are $30000, ROAS=3:1. **ARPU** is the average revenue per user. It’s calculated as total revenue / number of unique users. $60000 revenue / 5000 users = $12 ARPU. It’s helpful to compare this value to the cost of install (CPI) to understand the effectiveness of your marketing campaigns. **ARPPU** is the average revenue per paying user. It’s calculated as total revenue / number of unique paying users. $60000 revenue / 1000 paying users = $60 ARPPU. It helps you to understand how much money brings you a paying customer on average. **ARPAS** is the average revenue per active subscriber. It’s calculated as total revenue / number of active subscribers. By subscribers, we mean those who activated a trial period or subscription. $60000 revenue / 1500 subscribers = $40 ARPAS. ### Commission fees and taxes One important aspect of revenue calculation in cohorts is the inclusion of store commission fees and taxes (which can vary based on the user's store account country) and store commission fees. Adapty currently supports commission fee and taxes calculation for both App Store and Play Store in cohort analytics. For more details on how Adapty calculates taxes and commissions in its analytics, please refer to our [documentation](controls-filters-grouping-compare-proceeds#store-commission-and-taxes). ## Revenue vs Proceeds Both Revenue and Proceeds are money metrics. You can think of Revenue as gross revenue and Proceeds as net revenue. Revenue doesn't account for App Store / Play Store fees, while Proceeds do. Therefore Proceeds are always less than Revenue (15%-30% less to be exact). Apple and Google take up to 30% of the price paid by the customers as a fee. For the apps included in Small Business Program (i.e. the app makes less than $1m per year), the fee is always 15%. The rest of the apps (>$1m per year) pay 30% by default and 15% for subscriptions that are consecutively renewed for at least a year. This means 53+ renewals for weekly subscriptions, 13+ renewals for monthly subscriptions, and 2+ renewals for annual subscriptions. Adapty automatically determines the fee for every transaction your customers make and calculates Proceeds based on it. ### Prediction: Revenue and LTV **Predicted revenue** is an estimated total revenue a cohort of paying subscribers is expected to generate within the selected period after cohort creation. It is calculated by multiplying the predicted LTV of the cohort by the predicted number of paying users within the cohort. For example, if the predicted LTV is $50 and there are 100 paying users in a cohort, the Predicted Revenue would be $5,000. **Predicted LTV** is the estimated lifetime value per paying subscriber, representing the average revenue each paying subscriber is expected to generate within the selected period after cohort creation. These predictions are done using machine learning (ML) models, which analyze historical customer data to identify patterns and make predictions about future revenue. For detailed documentation on Adapty's prediction models, please refer to our [Prediction documentation](predicted-ltv-and-revenue). Adapty cohorts provide detailed insights into user behavior and financial performance within your app. By analyzing cohorts based on renewals or days, you can determine when cohorts become profitable, track revenue, calculate average revenue per user, and understand the time it takes to recoup advertising spend. With customizable filters, metrics, and export options, Adapty empowers you to make data-driven decisions and optimize user acquisition and monetization strategies for maximum app success. --- # File: analytics-conversion.md --- --- title: "Conversion analysis" description: "Measure subscription conversion rates using Adapty’s analytics tools." --- While funnels give you a high-level overview and retention focuses on user loyalty, conversion analysis is designed to help you evaluate effectiveness at every critical step in the user journey—over time. Conversions assist with the following questions: 1. How do app conversions change over time? Are there any seasonal trends? 2. How conversions are changed in the moment of marketing activities or some other new circumstances? 3. How do users in different regions respond to your app updates? 4. Which product types convert better over time? Conversion is performed with the data we gather through Adapty SDK and store notifications, and it doesn't require any additional configuration from your side. ## Main controls and charts Though revenue is often the go-to metric for measuring success, it's just one part of the bigger picture. Understanding how your business performs over time—across different user behaviors and lifecycle stages—is equally important. That’s where conversion analytics come into play. You can find more valuable insights about user behavior by setting filters and groups. To identify and analyze trends, monitor how your conversions evolve daily, monthly, or yearly. On the left side of the chart, you'll find the conversion steps control. This lets you choose which specific conversions to track—such as Install → Trial, Trial → Paid, or Paid → Renewal. Each conversion metric follows this logic: - Let **X** be the number of users who entered the starting state on a selected date (e.g., installs). - Let **Y** be the number of those users who eventually reached the target state (e.g., trial starts). - The conversion rate is calculated as: **Conversion = (Y / X) × 100%** :::note The date shown on the chart corresponds to when users entered the initial state (X)—the moment they became eligible to convert. ::: Please see below for each conversion explanation, along with an example for your reference. ### Install -> Paid This metric shows what percentage of users who installed the app on a specific date eventually purchased their first subscription.
How it works **Let**: - **X** = number of installs on a selected date (same for all products, as no product is chosen at the time of installation). - **Y** = number of those users who eventually purchased their first subscription (trial or non-trial). **Formula**: Conversion = (Y / X) × 100% **Example**: - On January 1, there were 100 installs. - By January 8, 20 of those users had subscribed. - On January 8, the conversion for January 1 = (20 / 100) × 100% = 20% - By February 1, 30 more users from the January 1 install group had purchased a subscription. - On February 1, the conversion for January 1 = ((20 + 30) / 100) × 100% = 50% This means that 50% of users who installed the app on January 1 eventually converted to a paid subscription, up to the current moment.
### Install -> Trial This metric shows the percentage of users who installed the app on a specific date and eventually started a trial.
How it works **Let**: - **X** = number of installs on a selected date (same for all products, as no product is chosen at the time of installation). - **Y** = number of those users who eventually activated a trial, at any time. **Formula**: Conversion = (Y / X) × 100% **Example**: - On January 1, there were 100 installs. - By January 8, 20 of those users had started a trial. - On January 8, the conversion for January 1 = (20 / 100) × 100% = 20% - By February 1, 30 more users from the January 1 install group had started a trial. - On February 1, the conversion for January 1 = ((20 + 30) / 100) × 100% = 50% This means that 50% of users who installed the app on January 1 eventually started a trial, up to the current moment.
### Trial -> Paid This metric shows the percentage of users who started a trial on a specific date and later purchased their first subscription.
How it works **Let**: - **X** = number of trials started on a selected date. - **Y** = number of those users who eventually purchased a subscription after their trial. **Formula**: Conversion = (Y / X) × 100% **Example**: - On January 1, 100 trials were started. - By January 8, 20 of those users had subscribed. - On January 8, the conversion for January 1 = (20 / 100) × 100% = 20% - By February 1, 30 more users from the January 1 trial group had subscribed. - On February 1, the conversion for January 1 = ((20 + 30) / 100) × 100% = 50% This means that 50% of users who started a trial on January 1 eventually converted to a paid subscription, up to the current moment.
### Paid -> 2nd Period This metric shows the percentage of users who renewed their subscription after the first payment.
How it works **Let**: - **X** = number of first-time subscriptions on a selected date. - **Y** = number of users who renewed for a second period, any time later (typically after one subscription cycle; includes grace period renewals). - **Formula**: Conversion = (Y / X) × 100% **Example**: - On January 1, there were 100 first-time subscriptions. - By January 8, 20 of those had renewed. - On January 8, the conversion for January 1 = (20 / 100) × 100% = 20% - By February 1, 30 more users from that group had renewed. - On February 1, the conversion for January 1 = ((20 + 30) / 100) × 100% = 50% This shows that 50% of users who made their first subscription payment on January 1 renewed for a second period, up to the current moment.
### 2nd Period -> 3rd Period This metric tracks how many users renewed again after their second subscription period.
How it works **Let**: - **X** = number of second-period subscriptions on a selected date. - **Y** = number of users who renewed for a third period, any time later (typically after one more billing cycle; includes grace period renewals). **Formula**: Conversion = (Y / X) × 100% **Example**: - On January 1, there were 100 second-period subscriptions. - By January 8, 20 of those users had renewed. - On January 8, the conversion for January 1 = (20 / 100) × 100% = 20% - By February 1, 30 more users had renewed. - On February 1, the conversion for January 1 = ((20 + 30) / 100) × 100% = 50% This shows that 50% of users who entered their second subscription period on January 1 renewed for a third, up to the current moment.
### 3rd Period -> 4th Period This metric shows the percentage of users who renewed after their third subscription period.
How it works **Let**: - **X** = number of third-period subscriptions on a selected date. - **Y** = number of users who renewed for a fourth period any time later (typically after one billing cycle; includes grace period renewals). **Formula**: Conversion = (Y / X) × 100% **Example**: - On January 1, there were 100 third-period subscriptions. - By January 8, 20 users had renewed. - On January 8, the conversion for January 1 = (20 / 100) × 100% = 20% - By February 1, 30 more users renewed. - On February 1, the conversion for January 1 = ((20 + 30) / 100) × 100% = 50% This means that 50% of users who entered their third subscription period on January 1 renewed for a fourth, up to the current moment.
### 4th Period -> 5 the Period This metric shows the percentage of users who renewed after their fourth subscription period.
How it works **Let**: - **X** = number of fourth-period subscriptions on a selected date. - **Y** = number of users who renewed for a fifth period any time later (typically after one billing cycle; includes grace period renewals). **Formula**: Conversion = (Y / X) × 100% **Example**: - On January 1, there were 100 fourth-period subscriptions. - By January 8, 20 users had renewed. - On January 8, the conversion for January 1 = (20 / 100) × 100% = 20% - By February 1, 30 more users renewed. - On February 1, the conversion for January 1 = ((20 + 30) / 100) × 100% = 50% This means that 50% of users who entered their fourth subscription period on January 1 renewed for a fifth, up to the current moment.
### 6 Months + This metric shows the percentage of users who remained subscribed for longer than 6 months from their first subscription.
How it works **Let**: - **X** = number of first-time subscriptions on a selected date. - **Y** = number of those users who renewed at least once after 6 months from the original subscription date. **Formula**: Conversion = (Y / X) × 100% **Example**: - On January 1, there were 100 first-time subscriptions. - By the first week of July, 20 of them renewed (e.g. on their 25th weekly subscription). - On July 8, the conversion for January 1 = (20 / 100) × 100% = 20% - By August 1, 30 more renewed after 6 months. - On August 1, the conversion for January 1 = ((20 + 30) / 100) × 100% = 50% This means that 50% of users who subscribed on January 1 remained subscribed past 6 months as of August 1.
### 1 Year + This metric shows the percentage of users who remained subscribed for longer than 12 months from their first subscription.
How it works **Let**: - **X** = number of first-time subscriptions on a selected date. - **Y** = number of those users who renewed at least once after 12 months from the original subscription date. **Formula**: Conversion = (Y / X) × 100% **Example**: - On January 1, 2021, there were 100 first-time subscriptions. - By the first week of January 2022, 20 had renewed. - On January 8, 2022, the conversion = (20 / 100) × 100% = 20% - By February 1, 2022, 30 more had renewed after 12 months. - On February 1, 2022, the conversion = ((20 + 30) / 100) × 100% = 50% This means that 50% of users who subscribed on January 1, 2021 stayed active for more than one year.
### 2 Years + This metric shows the percentage of users who stayed subscribed for more than 24 months from their first payment date.
How it works **Let**: - X = number of first-time subscriptions on a selected date. - Y = number of those users who renewed at least once after 24 months from the original subscription date. **Formula**: Conversion = (Y / X) × 100% **Example**: - On January 1, 2020, there were 100 first-time subscriptions. - By the first week of January 2022, 20 of them had renewed. - On January 8, 2022, the conversion = (20 / 100) × 100% = 20% - By February 1, 2022, 30 more had renewed after 2 years. - On February 1, 2022, the conversion = ((20 + 30) / 100) × 100% = 50% This means that 50% of the users who subscribed on January 1, 2020 were still active after 2 years, as of February 1, 2022.
## Grouping and time ranges The object for the analysis when the conversion is chosen is the chart. It performs how the conversion percentage changes over time. Using the date picker please select the quick options for the time period. The chart usually contains several curves. Up to five of them are selected by default in the list of grouping and you may change the selection by choosing the checkboxes in the area to the right of the chart. When you open the page for the first time the product duration is selected as a default grouping. Then your settings are saved in the cache and the next time you see the group you've recently selected. The following groupings are available: - Product - Country - Store - Paywall - Duration - Marketing attribution If a chosen date range is not enough to show any results, you may see a notification that offers a relevant date and an option to adjust the date range automatically so you may do it with one click. ## Table view, filters and CSV export A comparison of the curves gives a bright picture, and to get more use the table view below the chart. The table is synchronized with the chart so hovering over a column you see the associated pop-up over the curves. The grouping that was mentioned above changes both the charts and the table. Set quick filter by product or use other advanced ones, including Product, Country, Store, Duration, Attribution. We know that it's important to have an option to work with numbers the way you like. So on the right of the control panel, there's a button to export funnel data to CSV. You can then open it in Excel, or Google Sheets, or import it into your own analytical system to continue analysis and forecasting in your preferred environment. :::warning Be sure to indicate that your app is included in Small Business Program in [Adapty General Settings](https://app.adapty.io/settings/general). ::: --- # File: analytics-funnels.md --- --- title: "Funnel analysis" description: "Understand analytics funnels in Adapty to monitor user behavior and improve conversions." --- Adapty funnels are designed to assist you with such kinds of questions: 1. What percentage of installs is converted to paying clients? 2. What part of those who tried the product became loyal? 3. Which steps show high drop-off and need more attention? 4. Why do clients stop to pay? With a funnel chart, you may also find more insights about user behavior setting filters and groups. Funnels work with the data that we gather through SDK and store notifications and don't require any additional configuration from your side. :::note Funnels reflect install data according to your install definition in [App Settings](general.md#4-installs-definition-for-analytics). ::: ### Funnel chart step by step Let's go through the elements of a funnel to understand how to read the user journey on the chart. The 1st column (1) is the number of installs. It is shown as an absolute value (2) of total installations (not unique users) and also as 100% - the largest input number for further conversions relative calculation. If a user deletes an app and then installs it again two separate installs will be counted. A grey area nearby stands for transition parameters between steps. A conversion percent to the next step (Displayed paywall) is shown on a flag (3). Drop off percent and an absolute value of churn are shown below (4). The 2nd column (5) shows the number of users of the app who saw a paywall at least one time (6). They are taken only from those installs that happened in a selected period. If a user sees a paywall in the selected period but his install date is out of range his view is not counted. There is also a percentage of such views taken from the 1st step (7). You may notice that this percent is equal to the grey flag (3) of the 1st step. This equality takes place only for these first steps. We collect data for this step from all your paywalls that use the `logShowPaywall()` method. So please be sure to send every paywall view to Adapty using this method as described in the [docs](present-remote-config-paywalls#track-paywall-view-events). A grey area next to the 2nd column stands for transition. A conversion percent to the next step (Trial) is shown on a flag (8). Drop-off percent and the absolute value of churned customers after the paywall are shown below (9). The 3rd column (10) shows the number of trials activated on the paywalls by customers who installed the app within a selected period (11). If a filter is set to non-trial product(s) this value becomes zero and the column is empty. See also a percent of trials taken from the 1st step, showing the conversion from installs to trials (12). You may notice that this percent is not equal now to the grey flag (8) of the previous step conversion. This is because we compare the current value with the 1st step at the top of the chart and with the previous step on grey flags. So a grey area next to the 3rd column shows a conversion percent to the next step (Paid) which is displayed on a flag (13). Drop-off percent and absolute value of churned customers during a trial period are shown below (14). Starting from the trial you can hover on the step to see churn reasons. The 4th column shows the number of activated subscriptions (15). For products without trials, this number includes direct subscriptions from a paywall. For products with trials, it contains the number of trials converted into paid subscriptions. If you have both types of products, with trial and without, it will be a sum of both. The percent at the top shows the conversion from installs (16). The percent on a grey flag shows conversion to the next step (renewal to the 2nd period) (17). Drop off before the renewal to the 2nd period percent and absolute value are shown below the conversion (18). This step starts a sequence of steps with a similar structure. After the 2nd renewal comes the 3rd, then the 4th, etc. If there is enough data in your app history you may see dozens of periods using the horizontal scroll. The logic for these steps remains the same: - percent from installs at the top, - percent from the previous step at the bottom, - the absolute amount of renewal at the top, - the absolute amount of churn at the bottom, - a hover for churn reasons pop-up. ### Table view, filters and CSV export A funnel chart is enriched with data in a table to provide handy material for your work with numbers. This table repeats the approach of the funnel with some amendments. There are columns that show data on all steps except for the step of the 1st paid subscription. Instead of this one, there are two separate: Install -> Paid and Trial -> Paid. They display a core point of conversion when a free user becomes paying. It may seem that there is a product type division: Install -> Paid column shows only products without trials while the column Trial -> Paid contains only products with trials. But that's not exactly the way it works. Because we also consider those users whose trial has expired and they purchase a product with a trial like it doesn't have it at all. Diving deeper into numbers you will find filtering powerful tools for new hypotheses. Feel free to set conditions in different dimensions. Collect true insights based on data. Variate: 1. Product type - economy, length, etc 2. Time range. 3. Country segmentation. 4. Traffic attribution. 5. Store. Select Absolute #, Relative %, or both to view only necessary data. Finally, on the right of the control panel, there's a button to export funnel data to CSV. You can then open it in Excel, or Google Sheets, or import it into your own analytical system. :::warning Be sure to indicate that your app is included in Small Business Program in [Adapty General Settings](https://app.adapty.io/settings/general). ::: --- # File: analytics-integration.md --- --- title: "Analytics integrations" description: "Integrate analytics tools with Adapty to track and optimize user subscriptions." --- Adapty sends all [subscription events](events) to analytical services, such as [Amplitude](amplitude), [Mixpanel](mixpanel), and [AppMetrica](appmetrica). We can also send events to your server using [webhook](webhook) integration. The best thing about this is that you don't have to send any of the events, we'll do it for you. Just make sure to configure the integration in the Adapty Dashboard. Adapty supports the integration with the following 3d-party analytics services: - [Amplitude](amplitude) - [AppMetrica](appmetrica) - [Firebase and Google Analytics](firebase-and-google-analytics) - [Mixpanel](mixpanel) - [PostHog](posthog) - [SplitMetrics Acquire](splitmetrics) :::note Don't see your analytics provider? Let us know! [Write to the Adapty support](mailto:support@adapty.io) and we'll consider adding it. ::: ## Event properties Webhook events are sent in JSON format. All events follow the same structure, but their fields vary based on the event type, store, and your specific configuration. | Property | Type | Description | | ----------------------------- | ------------- | ------------------------------------------------------------ | | **profile_id** | uuid | Adapty user ID. | | **currency** | str | Local currency (defaults to USD). | | **price_usd** | float | Product price before Apple/Google cut. Revenue. | | **proceeds_usd** | float | Product price after Apple/Google cut. Net revenue. | | **net_revenue_usd** | float | Net revenue (income after Apple/Google cut and taxes) in USD. Can be empty. | | **price_local** | float | Product price before Apple/Google cut in local currency. Revenue. | | **proceeds_local** | float | Product price after Apple/Google cut in local currency. Net revenue. | | **transaction_id** | str | A unique identifier for a transaction such as a purchase or renewal. | | **original_transaction_id** | str | The transaction identifier of the original purchase. | | **purchase_date** | ISO 8601 date | The date and time of product purchase. | | **original_purchase_date** | ISO 8601 date | The date and time of the original purchase. | | **environment** | str | Can be _Sandbox_ or _Production_. | | **vendor_product_id** | str | Product ID in the Apple App Store, Google Play Store, or Stripe. | | **base_plan_id** | str | [Base plan ID](https://support.google.com/googleplay/android-developer/answer/12154973) in the Google Play Store or [price ID](https://docs.stripe.com/products-prices/how-products-and-prices-work#what-is-a-price) in Stripe. | | **event_datetime** | ISO 8601 date | The date and time of the event. | | **store** | str | Can be _app_store_ or _play_store_. | | **trial_duration** | str | Duration of a trial period in days. Sent in a format "{} days" , for example, "7 days". | | **cancellation_reason** | str |

A reason why the user canceled a subscription.

Can be

iOS & Android

_voluntarily_cancelled_, _billing_error_, _refund_

iOS

_price_increase_, _product_was_not_available_, _unknown_

Android

_new_subscription_replace_, _cancelled_by_developer_

| | **subscription_expires_at** | ISO 8601 date | The Expiration date of subscription. Usually in the future. | | **consecutive_payments** | int | The number of periods, that a user is subscribed to without interruptions. Includes the current period. | | **rate_after_first_year** | bool | Boolean indicates that a vendor reduces cuts to 15%. Apple and Google have 30% first-year cut and 15% after it. | | **promotional_offer_id** | str | ID of promotional offer as indicated in the Product section of the Adapty Dashboard | | **store_offer_category** | str | Can be _introductory_ or _promotional_. | | **store_offer_discount_type** | str | Can be _free_trial_, _pay_as_you_go_ or _pay_up_front_. | | **paywall_name** | str | Name of the paywall where the transaction originated. | | **paywall_revision** | int | Revision of the paywall where the transaction originated. The value is set to 1. | | **developer_id** | str | Developer (SDK) ID of the placement where the transaction originated. | | **ab_test_name** | str | Name of the A/B test where the transaction originated. | | **ab_test_revision** | int | Revision of the A/B test where the transaction originated. The value is set to 1. | | **cohort_name** | str | Name of the audience to which the profile belongs to. | | **profile_event_id** | uuid | Unique event ID that can be used for deduplication. | | **store_country** | str | The country sent to us by the store. | | **profile_ip_address** | str | Profile IP (can be IPv4 or IPv6, with IPv4 preferred when available). It is updated each time IP of the device changes. | | **profile_country** | str | Determined by Adapty, based on profile IP. | | **profile_total_revenue_usd** | float | Total revenue for the profile, refunds included. | | **variation_id** | uuid | Unique ID of the paywall where the purchase was made. | | **access_level_id** | str | Paid access level ID | | **is_active** | bool | Boolean indicating whether paid access level is active for the profile. | | **will_renew** | bool | Boolean indicating whether paid access level will be renewed. | | **is_refund** | bool | Boolean indicating whether transaction is refunded. | | **is_lifetime** | bool | Boolean indicating whether paid access level is lifetime. | | **is_in_grace_period** | bool | Boolean indicating whether profile is in grace period. | | **starts_at** | ISO 8601 date | Date and time when paid access level starts for the user. | | **renewed_at** | ISO 8601 date | Date and time when paid access will be renewed. | | **expires_at** | ISO 8601 date | Date and time when paid access will expire. | | **activated_at** | ISO 8601 date | Date and time when paid access was activated. | | **billing_issue_detected_at** | ISO 8601 date | Date and time of billing issue. | | **profile_has_access_level** | Bool | A boolean that indicates whether the profile has an active access level (Webhook only). | Each event has the following properties: `transaction_id, original_transaction_id, purchase_date, original_purchase_date, environment, vendor_product_id, event_datetime, store`. In addition, some events have additional properties. For the events `subscription_refunded` and `non_subscription_purchase_refunded`, it is mandatory to provide the values of `price_usd` and `proceeds_usd` as additional properties. | Event Name | Properties | | :---------------------------------- | :----------------------------------------------------------- | | **subscription\_initial\_purchase** | price\_usd, proceeds\_usd, subscription\_expires\_at, consecutive\_payments, rate\_after\_first\_year, trial\_duration | | **subscription\_renewed** | price\_usd, proceeds\_usd, subscription\_expires\_at, consecutive\_payments, rate\_after\_first\_year, trial\_duration | | **subscription\_cancelled** | cancellation\_reason, trial\_duration | | **trial\_started** | subscription\_expires\_at, trial\_duration | | **trial\_converted** | price\_usd, proceeds\_usd, subscription\_expires\_at, consecutive\_payments, rate\_after\_first\_year, trial\_duration | | **trial\_cancelled** | cancellation\_reason, trial\_duration | | **non\_subscription\_purchase** | price\_usd, proceeds\_usd | | **billing\_issue\_detected** | subscription\_expires\_at, trial\_duration | | **entered\_grace\_period** | subscription\_expires\_at, trial\_duration | Event example ```json title="Json" { "price_usd": 9.99, "proceeds_usd": 6.99, "transaction_id": "1000000628581600", "original_transaction_id": "1000000628581600", "purchase_date": "2020-02-18T18:40:22.000000+0000", "original_purchase_date": "2020-02-18T18:40:22.000000+0000", "environment": "Sandbox", "vendor_product_id": "premium", "event_datetime": "2020-02-18T18:40:22.000000+0000", "store": "app_store" } ``` Adapty sends events to your server and 3rd party analytical systems. **profile_ip_address** property is synchronized with the current device IP. Each time the Adapty servers receive info from the SDK, the IP will be updated if it differs from the one we have on record. ### Setting the profile's identifier - Set the profile's identifier for the selected analytics using the [iOS](setting-user-attributes.md), [Android](android-setting-user-attributes.md), [Flutter](flutter-setting-user-attributes.md), [React Native](react-native-setting-user-attributes.md), and [Unity](unity-setting-user-attributes.md). :::warning Avoiding duplication Don't forget to turn off sending subscription events from devices and your server to avoid duplication ::: ### Disabling external analytics for a specific customer You may want to stop sending analytics events for a specific customer. This is useful if you have an option in your app to opt-out of analytics services. To disable external analytics for a customer, use `updateProfile()` method. Create `AdaptyProfileParameters.Builder` object and set the corresponding value to it. When external analytics is blocked, Adapty won't be sending any events to any integrations for the specific user. If you want to disable an integration for all users of your app, just turn it off in Adapty Dashboard. ```swift showLineNumbers let builder = AdaptyProfileParameters.Builder() .with(analyticsDisabled: true) Adapty.updateProfile(parameters: builder.build()) ``` ```kotlin showLineNumbers val builder = AdaptyProfileParameters.Builder() .withExternalAnalyticsDisabled(true) Adapty.updateProfile(builder.build()) ``` ```java showLineNumbers ] AdaptyProfileParameters.Builder builder = new AdaptyProfileParameters.Builder() .withExternalAnalyticsDisabled(true); Adapty.updateProfile(builder.build()); ``` ```javascript showLineNumbers final builder = AdaptyProfileParametersBuilder() ..setAnalyticsDisabled(true); try { await Adapty().updateProfile(builder.build()); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ```csharp showLineNumbers var builder = new AdaptyProfileParameters.Builder() .SetAnalyticsDisabled(true); Adapty.UpdateProfile(builder.Build(), (error) => { if(error != null) { // handle the error } }); ``` ```typescript showLineNumbers adapty.updateProfile({ analyticsDisabled: true }); ``` ### Disable collection of advertising identifiers You can disable IDFA collection by using the `idfaCollectionDisabled` property. Make sure you call it before `.activate()` method. ```swift showLineNumbers // In your AppDelegate class: let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") // highlight-start .with(idfaCollectionDisabled: true) // set to `true` // highlight-end Adapty.activate(with: configurationBuilder.build()) { error in // handle the error } ``` You can disable AAID/GAID collection by using the `withAdIdCollectionDisabled` property when activating the Adapty SDK: ```swift showLineNumbers override fun onCreate() { super.onCreate() Adapty.activate( applicationContext, AdaptyConfig.Builder("PUBLIC_SDK_KEY") // highlight-start .withAdIdCollectionDisabled(true) // set to `true` // highlight-end .build() ) } ``` You can disable AAID/GAID collection by using the `withAdIdCollectionDisabled` property when activating the Adapty SDK: ```swift showLineNumbers  @Override public void onCreate() { super.onCreate(); Adapty.activate( applicationContext, new AdaptyConfig.Builder("PUBLIC_SDK_KEY") // highlight-start .withAdIdCollectionDisabled(true) // set to `true` // highlight-end .build() ); } ``` You can disable IDFA collecting by using the `withAppleIdfaCollectionDisabled` property and Google/Android Advertising ID by using the `withGoogleAdvertisingIdCollectionDisabled` property. Set them to `true` when activating the Adapty SDK: ```dart showLineNumbers try { await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_API_KEY') // highlight-start ..withGoogleAdvertisingIdCollectionDisabled(true), // set to `true` ..withAppleIdfaCollectionDisabled(true), // set to `true` // highlight-end ); } catch (e) { // handle the error } ``` You can disable IDFA collecting by using the `SetIDFACollectionDisabled` property when activating the Adapty SDK. The AAID/GAID collection cannot be disabled now. ```dart showLineNumbers var builder = new AdaptyConfiguration.Builder("YOUR_API_KEY") // highlight-start .SetIDFACollectionDisabled(true); // set to `true` // highlight-end Adapty.Activate(builder.Build(), (error) => { // handle the error } ``` You also can disable IDFA collecting by using the `idfaCollectionDisabled` property when activating the Adapty SDK or disable the AAID/GAID collection by using the `adIdCollectionDisabled` property. ```typescript showLineNumbers adapty.activate('PUBLIC_SDK_KEY', { // highlight-start ios: { idfaCollectionDisabled: true, // set to `true` }, android: { adIdCollectionDisabled: true, }, // highlight-end }); ``` --- # File: analytics-retention.md --- --- title: "Retention analysis" description: "Understand user retention analytics and optimize your subscription strategy." --- Retention charts can help with the following questions: 1. How does your app retain clients from period to period? 2. What products are more attractive and hold better? 3. What groups of users are more loyal? 4. Which level of retention can be used as a benchmark for growth? 5. And of course, how can you save money investing in the attracted audience instead of capturing new. You'll find valuable insights about user behavior setting filters and groups. Retention is performed with the data that we gather through SDK and store notifications and don't require any additional configuration from your side. ### How do we calculate retention? Observing the retention chart, you see how the number of users depends on the step they take: trial (if the checkbox "show trials" is checked), the 1st payment, the 2nd payment, etc. Let's specify what users are counted when you choose a date range for the retention chart. For example, you've selected the last 3 months in the calendar, and the checkbox "show trials" is unchecked. This means we count only those who have had their 1st subscription during the last 3 months. If the checkbox "show trials" is checked and the 3 last months are selected in the calendar we count all those who have had their trials during the last 3 months. For these subscribers, we show the absolute retention for the Nth step as the number of those who have had the Nth payment. And we calculate a relative value of retention for the Nth step as a ratio of the absolute amount of the Nth payment to the total amount of subscriptions (or trials) during the selected time range. :::info Retention changes retrospectively Regardless of when you check the chart, the baseline number (100%) remains the same for the selected period of time. Meanwhile, the retention to the next period may grow over time. For example, for a Monthly subscription, if there are 20 first purchases made between Dec 1 and Dec 31, it is expected that retention to the second period will grow throughout January (and possibly even after) while users will be entering the next subscription period in time or later for some reasons (e.g. grace period). ::: ### Retention opportunities Let's see how to get more from the Adapty retention feature. Having not only a pure passion for numbers but more willingly seeing real business value after implementing analytical results, we may think about the purposes first. With a deep dive into chart features, it would be nice to clear up the impact this data can have. So let's keep in a glance together WHY and HOW. 1 - work with the audience. First of all, retention is about the target audience, its preferences, and whether your product meets their expectations or not during the consuming lifetime. If you have wondered how to measure the core relationship of your business that generates money - retention is at your service. Such a measurement benefits because it's usually cheaper to sell to your customer than to a stranger. And this cost is low for two reasons: less effort to sell and higher average check. So it might be a good idea to invest in your subscribers' loyalty when retention goes down. 2 - work with the product. The second reason WHY is that retention charts show the actual consuming lifetime of your product and let you forecast in long term. And if you want to improve, correct the job that delivers the product to change its lifetime, and then forecast again to become closer to your business targets. Such updates may be a part of a strategic vision working together with a forecasting routine. And yes, this process never ends because we all run fast to be at the same place in a constantly changing environment. 3 - work with the market. Moving faster than the main competitors is good but sometimes jumping out of the ordinary race may bring more benefits. When you analyze the behavior of users in different countries and stores, some local peculiarities can open outstanding insights and new opportunities for the business. Cultural and market context can be analyzed from the perspective of retention to be later used for segmentation and further development. For example, you may find blue water in some regions and grow there faster. The usage of retention data is of course, not limited to this basic interpretation but it may be a good start if you want to get real value fast. ### Curves, table view, filters and CSV export Now when we are on the same page with retention purposes and basic ways of interpretation let's go through the tools that make it all handy. The core of the retention feature in Adapty is the chart. It shows how retention level depends on the steps of a customer's lifetime. The steps are shown on the horizontal axis: Trial, Paid (the 1st subscription), P2 (the 2nd subscription), P3, P4, etc Please mind that the axis starts with the Trial step only when the checkbox "Show trials" is selected. For data calculation, this checkbox works as follows. When "Show trials" is selected and the axis starts with the Trial step, you see only scenarios that contain trials, no transactions directly from installs are shown and the step Paid contains only transactions that come from trials. When "Show trials" is not selected, and the axis starts with a Paid step, this first step contains all first transactions including both from trials and directly from installs. When you hover over the chart, a pop-up with a data summary is displayed. And if you hover over a column in the table below, you also see a summary pop-up with relevant data on the chart. The table contains the same grouping and filters chosen for the chart. Feel free to combine filters and grouping for advanced analysis. Collect true insights based on data. Variate: 1. Product type. 2. Duration. 3. Time range. 4. Country. 5. Traffic attribution. 6. Store. Use #Absolute and %Relative control to view the necessary data. Finally, on the right of the control panel, there's a button to export funnel data to CSV. You can then open it in Excel, or Google Sheets, or import it into your own analytical system to continue analysis and forecasting in your preferred environment. :::warning Be sure to indicate that your app is included in Small Business Program in [Adapty General Settings](https://app.adapty.io/settings/general). ::: --- # File: android-check-subscription-status.md --- --- 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-display-legacy-pb-paywalls.md --- --- title: "Display legacy Paywall Builder paywalls in Android SDK" description: "Learn how to display legacy Paywall Builder paywalls in your Android app with Adapty SDK." displayed_sidebar: sdkandroid --- This page contains guides for displaying legacy Paywall Builder paywalls in your Android app. Choose the topic you need: - **[Fetch legacy Paywall Builder paywalls](android-get-legacy-pb-paywalls)** - Retrieve legacy paywalls and their configuration - **[Present legacy Paywall Builder paywalls](android-present-paywalls-legacy)** - Display legacy paywalls to users - **[Handle legacy paywall events](android-handling-events-legacy)** - Manage legacy paywall interactions --- # File: android-get-legacy-pb-paywalls.md --- --- title: "Fetch legacy Paywall Builder paywalls in Android SDK" description: "Retrieve legacy PB paywalls in your Android app with Adapty SDK." displayed_sidebar: sdkandroid --- 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! 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 should hardcode is the placement ID. Response parameters: | Parameter | Description | | :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](android-sdk-models#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-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-get-onboardings.md --- --- title: "Get onboardings in Android SDK" description: "Learn how to retrieve onboardings in Adapty for Android." displayed_sidebar: sdkandroid --- 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`](sdk-models#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). ::: ```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-get-pb-paywalls.md --- --- 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." displayed_sidebar: sdkandroid --- 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. 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-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`.

| 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 should hardcode is the placement ID. Response parameters: | Parameter | Description | | :-------- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------| | Paywall | An [`AdaptyPaywall`](android-sdk-models#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). 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 you have successfully loaded the paywall and its view configuration, you can present it in your mobile app. ## 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-handle-onboarding-events.md --- --- title: "Handle onboarding events in Android SDK" description: "Handle onboarding-related events in Android using Adapty." toc_max_heading_level: 4 --- 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 } } ```
## Updating field state 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 saveUserPreference(elementId = action.elementId, value = params.value) } is AdaptyOnboardingStateUpdatedParams.MultiSelect -> { // Handle multiple selections saveUserPreferences(elementId = action.elementId, values = params.map { it.value }) } is AdaptyOnboardingStateUpdatedParams.Input -> { // Handle text input saveUserInput(elementId = action.elementId, value = params.value) } is AdaptyOnboardingStateUpdatedParams.DatePicker -> { // Handle date selection saveUserDate(elementId = action.elementId, value = "${params.month}-${params.day}-${params.year}") } } } ``` :::note This example suggests you implement custom methods for saving user data depending on the data type. These methods are not built into the Adapty SDK. ::: The `action` object contains: - `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, which can be one of the following types: - `Select`: Single selection from a list of options. - `MultiSelect`: Multiple selections from a list of options. - `Input`: Text input from the user. - `DatePicker`: Date selected by the user.
Saved data examples (Click to expand) ```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 } } } ```
## 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-handle-paywall-actions.md --- --- title: "Respond to button actions in Android SDK" description: "Handle paywall button actions in Android using Adapty for better app monetization." toc_max_heading_level: 4 --- 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 `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-legacy.md --- --- title: "Handle paywall events in legacy Android SDK" description: "Handle subscription events in Android (Legacy) with Adapty SDK." toc_max_heading_level: 4 --- 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. --- # File: android-handling-events.md --- --- title: "Android - Handle paywall events" description: "Handle Android subscription events efficiently with Adapty's event tracking tools." toc_max_heading_level: 4 --- :::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 `Adapty.makePurchase()` 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 `Adapty.makePurchase()` fails, this method will be invoked: ```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. #### Successful restore If `Adapty.restorePurchases()` 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-identifying-users.md --- --- title: "Identify users in Android SDK" description: "Identify users in Adapty to improve personalized subscription experiences (Android)." displayed_sidebar: sdkandroid --- 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-implement-paywalls-manually.md --- --- title: "Implement paywalls manually in Android SDK" description: "Learn how to implement paywalls manually in your Android app with Adapty SDK." displayed_sidebar: sdkandroid --- This page contains guides for implementing paywalls manually in your Android app. Choose the topic you need: - **[Fetch paywalls and products](fetch-paywalls-and-products-android)** - Retrieve paywalls and product data - **[Present remote config paywalls](present-remote-config-paywalls-android)** - Display remote config paywalls - **[Accept purchases](android-making-purchases)** - Handle purchase transactions - **[Restore purchases](android-restore-purchase)** - Restore previous purchases - **[Implement Observer mode](implement-observer-mode-android)** - Set up Observer mode for analytics and paywall integration - **[Report transactions in Observer Mode](report-transactions-observer-mode-android)** - Report purchase transactions in Observer Mode - **[Present Paywall Builder paywalls in Observer mode](android-present-paywall-builder-paywalls-in-observer-mode)** - Display Paywall Builder paywalls in Observer mode - **[Troubleshooting](android-troubleshoot-purchases)** - Resolve common purchase issues --- # File: android-legacy-install.md --- --- 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-legacy.md --- --- title: "Legacy Android SDK guides" description: "Legacy documentation for Adapty Android SDK." displayed_sidebar: sdkandroid --- This page contains legacy documentation for Adapty Android SDK. Choose the topic you need: - **[Legacy installation guide](android-legacy-install)** - Install and configure legacy Android SDK - **[Display legacy Paywall Builder paywalls](android-display-legacy-pb-paywalls)** - Work with legacy paywall builder --- # File: android-listen-subscription-changes.md --- --- 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](sdk-models#adaptyprofile) 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](sdk-models#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: ```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: android-localizations-and-locale-codes.md --- --- title: "Use localizations and locale codes in Android SDK" description: "Manage app localizations and locale codes to reach a global audience (Android)." displayed_sidebar: sdkandroid --- ## 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-making-purchases.md --- --- title: "Make purchases in mobile app in Android SDK" description: "Guide on handling in-app purchases and subscriptions using Adapty." displayed_sidebar: sdkandroid --- 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 In paywalls built with [Paywall Builder](adapty-paywall-builder) purchases are processed automatically with no additional code. If that's your case — you can skip this step. ::: ```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`](sdk-models#adaptypaywallproduct) object retrieved from the paywall. | Response parameters: | Parameter | Description | |---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Profile** |

If the request has been successful, the response contains this object. An [AdaptyProfile](sdk-models#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 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`](sdk-models#adaptysubscriptionupdateparameters) 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`](sdk-models#adaptysubscriptionupdateparameters) 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. ## Set obfuscated account and profile IDs Google Play requires obfuscated account and profile 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 Adapty.makePurchase( activity, product, AdaptyPurchaseParameters.Builder() .withObfuscatedAccountId("YOUR_OBFUSCATED_ACCOUNT_ID") .withObfuscatedProfileId("YOUR_OBFUSCATED_PROFILE_ID") .build() ) { result -> // Handle result } ``` ```java showLineNumbers Adapty.makePurchase( activity, product, new AdaptyPurchaseParameters.Builder() .withObfuscatedAccountId("YOUR_OBFUSCATED_ACCOUNT_ID") .withObfuscatedProfileId("YOUR_OBFUSCATED_PROFILE_ID") .build(), result -> { // Handle result } ); ``` --- # File: android-onboardings.md --- --- title: "Onboardings in Android SDK" description: "Learn how to work with onboardings in your Android app with Adapty SDK." displayed_sidebar: sdkandroid --- This page contains all guides for working with onboardings in your Android app. Choose the topic you need: - **[Get onboardings](android-get-onboardings)** - Retrieve onboardings from Adapty - **[Display onboardings](android-present-onboardings)** - Present onboardings to users - **[Handle onboarding events](android-handle-onboarding-events)** - Manage onboarding interactions --- # File: android-paywalls.md --- --- title: "Paywalls in Android SDK" description: "Learn how to work with paywalls in your Android app with Adapty SDK." displayed_sidebar: sdkandroid --- This page contains all guides for working with paywalls in your Android app. Choose the topic you need: - **[Get paywalls](android-get-pb-paywalls)** - Retrieve paywalls from Adapty - **[Display paywalls](android-present-paywalls)** - Present paywalls to users - **[Handle paywall events](android-handling-events)** - Manage paywall interactions - **[Work with paywalls offline](android-use-fallback-paywalls)** - Use fallback paywalls when offline - **[Localize paywalls](android-localizations-and-locale-codes)** - Support multiple languages - **[Implement paywalls manually](android-implement-paywalls-manually)** - Build custom paywall UI --- # File: android-present-onboardings.md --- --- 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. --- # File: android-present-paywall-builder-paywalls-in-observer-mode.md --- --- 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 `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 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`](sdk-models#adaptypaywall) 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), [Flutter](sdk-installation-flutter#configure-adapty-sdk), [React Native](sdk-installation-reactnative#configure-adapty-sdks), 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`](sdk-models#adaptypaywall) 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 //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 ... 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-present-paywalls-legacy.md --- --- 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-present-paywalls.md --- --- 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) ::: 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-products.md --- --- title: "Product in Play Store" description: "Manage Android products with Adapty, streamline in-app purchases, and optimize monetization strategies." --- This page provides guidance on creating a product in Play Store. While this information may not directly pertain to Adapty's functionality, it serves as a valuable resource if you encounter challenges while creating products in Google Play console. Product refers to a digital item or service that you offer within your app in Play Store, typically available for purchase. These can include in-app products such as one-time purchases, subscriptions, or other digital goods that users can acquire while using your application. In the [Google’s billing system](https://developer.android.com/google/play/billing/compatibility), subscriptions can incorporate multiple base plans, each providing various discounts or offers. This structure is comprised of three main components: - **Subscriptions:** These represent sets of benefits that users can enjoy for a specific period (the items being sold). For instance, a "Gold tier" providing premium features for subscribers. - **Base plans:** These represent specific configurations of billing periods, renewal types, and prices (how the items are sold). Examples include "annual with auto-renewal" or "prepaid monthly." - **Offers:** These entail discounts available to eligible users, modifying the base plan's price. For instance, a "free 14-day trial for new users." ## How to create a product in Play Store? Product refers to a digital item or service that you offer within your app, typically available for purchase. These can include in-app products such as one-time purchases, subscriptions, or other digital goods that users can acquire while using your application. To set up a product for Android devices: 1. Open [**Monetize** -> **Subscriptions**](https://console.cloud.google.com/iam-admin/serviceaccounts) or [**Monetize** -> **In-app products**](https://console.cloud.google.com/iam-admin/serviceaccounts) section in the left menu of the Google Play Console. 2. Click the **Create subscription** button. 3. In the opened **Create subscription** window, enter the subscription ID in the **Product ID** field and the subscription name in the **Name** field. Product ID has to be unique and must start with a number or lowercase letter, and can also contain underscores (\_), and periods (.). It is used to access your product during development and synchronize it with Adapty. Once a Product ID is assigned to a product in the Google Play Console, it cannot be reused for any other apps, even if the product is deleted. When naming your product ID, it is advisable to follow a standardized format. We recommend using a more concise approach and naming the product`.`. Then, you can control the duration and billing frequency through the use of base plans, such as weekly, monthly, and so on. The Name is used for your reference only, it will be visible on your Google Play Store listing, so feel free to use any descriptive name you need. It is limited to 55 characters. 4. Click the **Create** button to confirm the subscription creation. :::note Google Play subscription products in Adapty Adapty products correspond to Base Plans for Google Play subscriptions since those are the products available for customers to purchase. Adapty seamlessly handles the migration of existing Google Play subscriptions along with their corresponding base plans in products, requiring no additional action from you. However, when you add a new product in Adapty, you will be responsible for providing both the base plan ID and the product ID. ::: ### Create a base plan For subscription products, you'll need to add a base plan. Base plans determine the billing period, price, and renewal type for customers to purchase your subscription. Please note that customers do not directly purchase a subscription product. Instead, they always buy a base plan within a subscription. To create a base plan: 1. Open [**Monetize** -> **Subscriptions**](https://console.cloud.google.com/iam-admin/serviceaccounts) section in the left menu of the Google Play Console. Once there, locate the subscription to which you'd like to add a base plan. 2. Click the **View subscription** button next to the subscription. 3. After the subscription details open. click on the **Add base plan** button under the **Base plans and offers** title. You may need to scroll down to find it. 4. In the opened **Add base plan** window, enter a unique identifier for the base plan in the Base **Plan ID** field. It must start with a number or lowercase letter, and can contain numbers (0-9), lowercase letters (a-z) and hyphens (-). and complete the required fields. 5. Specify the prices per region. 6. Сlick the **Save** button to finalize the setup. 7. Сlick the **Activate** button to make the baseline active. Keep in mind that subscription products can only have a single base plan with consistent duration and renewal type in Adapty. ### Fallback products :::warning Support for non backwards-compatible base plans Older versions of Adapty SDKs do not support Google Billing Library v5+ features, specifically multiple base plans per subscription product and offers. Only base plans marked as **[backwards compatible](https://support.google.com/googleplay/android-developer/answer/12124625?hl=en#backwards_compatible)** in the Google Play Console are accessible with these SDK versions. Note that only one base plan per subscription can be marked as backwards compatible. ::: To fully leverage the enhanced Google subscription configurations and features in Adapty, we offer the capability to set up a backward compatible fallback product. This fallback product is exclusively utilized for apps using older versions of the Adapty SDK. When creating Google Play products, you now have the option to indicate whether the product should be marked as backward compatible in the Play Console. Adapty utilizes this information to determine whether the product can be purchased by older versions of the SDK (versions 2.5 and below). Suppose you have a subscription named `subscription.premium` that offers two base plans: weekly (backward compatible) and monthly. If you add `subscription.premium:weekly` product to Adapty, you don't need to indicate a backward compatible product. However, in the case of `subscription.premium:monthly` product, you will need to specify a backward compatible product. Failing to do so could result in an unintended purchase of `subscription.premium:weekly` product in Google 4th billing library. To address this scenario, you should create a separate product where the base plan is also monthly and marked as backward compatible. This ensures that users who select the `subscription.premium:monthly` option will be billed correctly at the intended frequency. ## Add products to Adapty Once you have completed adding your in-app purchases, subscriptions, and offers in App Store Connect, the next step is to [add these products to Adapty](create-product). --- # File: android-quickstart-identify.md --- --- 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). ## 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, created anonymous profiles won't affect the dashboard [analytics](analytics-charts.md), because installs will be counted by new device IDs. However, if you want to change this behavior and count new customer user IDs instead of device IDs, go to **App settings** and set up [**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: android-quickstart-paywalls.md --- --- 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-making-purchases). | | 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. ## 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. 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-reference.md --- --- title: "Reference for Android SDK" description: "Reference documentation for Adapty Android SDK." displayed_sidebar: sdkandroid --- This page contains reference documentation for Adapty Android SDK. Choose the topic you need: - **[SDK models](android-sdk-models)** - Data models and structures used by the SDK - **[Handle errors](android-sdk-error-handling)** - Error handling and troubleshooting - **[Android SDK reference](https://kotlin.adapty.io)** - Complete API documentation --- # File: android-restore-purchase.md --- --- 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`](sdk-models#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: android-sdk-error-handling.md --- --- 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 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: android-sdk-migration-guides.md --- --- title: "Android SDK Migration Guides" description: "Migration guides for Adapty Android SDK versions." --- This page contains all migration guides for Adapty Android SDK. Choose the version you want to migrate to for detailed instructions: - **[Migrate to v. 3.4](migration-to-android-sdk-34)** - **[Migrate to v. 3.3](migration-to-android330)** - **[Migrate to v. 3.0](migration-to-android-sdk-v3)** --- # File: android-sdk-models.md --- --- title: "Android SDK Models" description: "Understand Adapty's SDK models to optimize in-app purchase handling (Android)." displayed_sidebar: sdkandroid --- ## Interfaces ### AdaptyOnboarding Information about an [onboarding](onboardings.md). | Name | Type | Description | |-------------------|---------------------------------------------------------------------|--------------------------------------------------------| | placement | [AdaptyPlacement](#adaptyplacement) | A placement, configured in Adapty Dashboard | | hasViewConfiguration | boolean | If true, it is possible to fetch the view object and use it with AdaptyUI library | | name | string | Name of the onboarding flow | | remoteConfig | [AdaptyRemoteConfig](#adaptyremoteconfig) (optional) | A remote config configured in Adapty Dashboard for this onboarding | | variationId | string | An identifier of a variation, used to attribute purchases to this onboarding | ### AdaptyPaywallProduct An information about a [product.](https://swift.adapty.io/documentation/adapty/adaptypaywallproduct) | Name | Type | Description | |:-----------------------------------------|:------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | vendorProductId | string | Unique identifier of a product from App Store Connect or Google Play Console | | adaptyProductId | string | Unique identifier of the product in Adapty | | paywallVariationId | string | Same as variationId property of the parent AdaptyPaywall | | paywallABTestName | string | Same as abTestName property of the parent AdaptyPaywall | | paywallName | string | Same as name property of the parent AdaptyPaywall | | localizedDescription | string | A description of the product | | localizedTitle | string | The name of the product | | price | [AdaptyPaywallProduct.Price](#adaptypaywallproductprice) (optional) | The cost of the product in the local currency | | subscriptionDetails | [AdaptyProductSubscriptionDetails](#adaptyproductsubscriptiondetails) (optional) | Detailed information about subscription (intro, offers, etc.) | ### AdaptyPaywallProduct.Price | Name | Type | Description | | :---------------- | :--------------- | :------------------------------------------------------------------------------------------------------------------------------- | | amount | number | Price as number | | currencyCode | string (optional) | The currency code of the locale used to format the price of the product. The ISO 4217 (USD, EUR) | | currencySymbol | string (optional) | The currency symbol of the locale used to format the price of the product. ($, €) | | localizedString | string (optional) | A price's language is determined by the preferred language set on the device. On Android, the formatted price from Google Play as is | ### AdaptyProductSubscriptionDetails | Name | Type | Description | |:------------------------------|:------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | subscriptionPeriod | [AdaptyProductSubscriptionPeriod](#adaptyproductsubscriptionperiod) | The period details for products that are subscriptions. Will be null for iOS version below 11.2 and macOS version below 10.14.4. | | localizedSubscriptionPeriod | string (optional) | The period's language is determined by the preferred language set on the device | | offer | [AdaptyProductDiscountPhase](#adaptyproductdiscountphase) (optional) | A subscription offer if available for the auto-renewable subscription | | basePlanId | string | The identifier of the base plan. Android Only. | | renewalType | string (optional) | The renewal type. Possible values: 'prepaid', 'autorenewable'. Android Only. | ### AdaptyProductSubscriptionPeriod | Name | Type | Description | | :------------ | :--------------- | :------------------------------------------------------------------------------------------------------------------------------- | | numberOfUnits | number | A number of period units | | unit | ProductPeriod | A unit of time that a subscription period is specified in. The possible values are: `day`, `week`, `month`, `year` | ### AdaptyProductDiscountPhase | Name | Type | Description | |:------------------------------|:------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | localizedNumberOfPeriods | string (optional) | A formatted number of periods of a discount for a user's locale | | localizedSubscriptionPeriod | string (optional) | A formatted subscription period of a discount for a user's locale | | numberOfPeriods | number | A number of periods this product discount is available | | price | [AdaptyPaywallProduct.Price](#adaptypaywallproductprice) | Discount price of a product in a local currency | | subscriptionPeriod | [AdaptyProductSubscriptionPeriod](#adaptyproductsubscriptionperiod) | An information about period for a product discount | | paymentMode | OfferType | A payment mode for this product discount. Possible values: `free_trial`, `pay_as_you_go`, `pay_up_front` | ### AdaptyPaywall An information about a [paywall.](https://swift.adapty.io/documentation/adapty/adaptypaywall) | Name | Type | Description | |--------------------|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | placement | [AdaptyPlacement](#adaptyplacement) | A placement, configured in Adapty Dashboard | | hasViewConfiguration | boolean | If true, it is possible to fetch the view object and use it with AdaptyUI library | | name | string | A paywall name | | remoteConfig | [AdaptyRemoteConfig](#adaptyremoteconfig) (optional) | A remote config configured in Adapty Dashboard for this paywall | | variationId | string | An identifier of a variation, used to attribute purchases to this paywall | ### AdaptyPlacement | Name | Type | Description | | :------------ | :--------------- | :------------------------------------------------------------------------------------------------------------------------------- | | abTestName | string | Parent A/B test name | | audienceName | string | A name of an audience to which the paywall belongs | | id | string | ID of a placement configured in Adapty Dashboard | | revision | number | Current revision (version) of a paywall. Every change within a paywall creates a new revision | | isTrackingPurchases | boolean (optional) | Whether the placement is tracking purchases | | audienceVersionId | string | Version ID of the audience | ### AdaptyRemoteConfig | Name | Type | Description | | :------------ | :--------------- | :------------------------------------------------------------------------------------------------------------------------------- | | lang | string | Identifier of a paywall locale | | data | object | A custom dictionary configured in Adapty Dashboard for this paywall | | dataString | string | A custom JSON string configured in Adapty Dashboard for this paywall | ### AdaptyProfile An information about a [user's](https://swift.adapty.io/documentation/adapty/adaptyprofile) subscription status and purchase history. | Name | Type | Description | | :--------------- | :---------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------- | | profileId | string | An identifier of the user in Adapty | | customerUserId | string (optional) | An identifier of the user in your system | | customAttributes | object | Previously set user custom attributes with the updateProfile method | | accessLevels | object\ | The keys are access level identifiers configured by you in Adapty Dashboard. The values are AccessLevel objects. Can be null if the customer has no access levels | | subscriptions | object\ | The keys are product ids from App Store Connect. The values are Subscription objects. Can be null if the customer has no subscriptions | | nonSubscriptions | object\ | The keys are product ids from App Store Connect. The values are arrays of NonSubscription objects. Can be null if the customer has no purchases | ### AdaptyProfile.AccessLevel Information about the [user's access level.](https://swift.adapty.io/documentation/adapty/adaptyprofile/accesslevel) | Name | Type | Description | |----|----|-----------| | id | string | Unique identifier of the access level configured by you in Adapty Dashboard | | isActive | boolean | Whether the access level is active. Generally, you have to check just this property to determine if the user has access to premium features | | vendorProductId | string | The identifier of the product in the App Store Connect that unlocked this access level | | store | string | The store of the purchase that unlocked this access level. The possible values are: play_store, adapty | | activatedAt | string (ISO 8601 datetime) | The time when the access level was activated | | renewedAt | string (ISO 8601 datetime) (optional) | The time when the access level was renewed | | expiresAt | string (ISO 8601 datetime) (optional) | The time when the access level will expire (could be in the past and could be null for lifetime access) | | isLifetime | boolean | Whether the access level is active for a lifetime (no expiration date). If set to true you shouldn't check expires_at, or you could just check isActive | | activeIntroductoryOfferType | string (optional) | The type of active introductory offer. Possible values are: free_trial, pay_as_you_go, pay_up_front. If the value is not null, it means that the offer was applied during the current subscription period | | activePromotionalOfferType | string (optional) | The type of active promotional offer. Possible values are: free_trial, pay_as_you_go, pay_up_front. If the value is not null, it means that the offer was applied during the current subscription period | | activePromotionalOfferId | string (optional) | An identifier of active promotional offer | | offerId | string (optional) | An identifier of a discount offer in Google Play that unlocked this access level | | willRenew | boolean | Whether the auto-renewable subscription is set to renew | | isInGracePeriod | boolean | Whether the auto-renewable subscription is in the grace period | | unsubscribedAt | string (ISO 8601 datetime) (optional) | The time when the auto-renewable subscription was cancelled. Subscription can still be active, it just means that auto-renewal turned off. Will be set to null if the user reactivates the subscription | | billingIssueDetectedAt | string (ISO 8601 datetime) (optional) | The time when billing issue was detected. Subscription can still be active. Will be set to null if the charge will be made | | startsAt | string (ISO 8601 datetime) (optional) | The time when this access level has started (could be in the future) | | cancellationReason | string (optional) | The reason why the subscription was cancelled. Possible values are: voluntarily_cancelled, billing_error, refund, price_increase, product_was_not_available, unknown | | isRefund | boolean | Whether the purchase was refunded | ### AdaptyProfile.Subscription Information about the [user's subscription.](https://swift.adapty.io/documentation/adapty/adaptyprofile/subscription) | Name | Type | Description | |----|----|-----------| | store | string | The store of the purchase. The possible values are: play_store, adapty | | vendorProductId | string | The identifier of the product in the App Store Connect | | vendorTransactionId | string | Transaction id from the App Store | | vendorOriginalTransactionId | string | Original transaction id from the App Store. For auto-renewable subscription, this will be the id of the first transaction in the subscription | | isActive | boolean | Whether the subscription is active | | isLifetime | boolean | Whether the subscription is active for a lifetime (no expiration date). If set to true you shouldn't check expires_at, or you could just check isActive | | activatedAt | string (ISO 8601 datetime) | The time when the subscription was activated | | renewedAt | string (ISO 8601 datetime) (optional) | The time when the subscription was renewed | | expiresAt | string (ISO 8601 datetime) (optional) | The time when the subscription will expire (could be in the past and could be null for lifetime access) | | startsAt | string (ISO 8601 datetime) (optional) | The time when the subscription has started (could be in the future) | | unsubscribedAt | string (ISO 8601 datetime) (optional) | The time when the auto-renewable subscription was cancelled. Subscription can still be active, it just means that auto-renewal turned off. Will be set to null if a user reactivates the subscription | | billingIssueDetectedAt | string (ISO 8601 datetime) (optional) | The time when billing issue was detected (Apple was not able to charge the card). Subscription can still be active. Will be set to null if the charge will be made | | isInGracePeriod | boolean | Whether the auto-renewable subscription is in the grace period | | isSandbox | boolean | Whether the product was purchased in the sandbox environment | | isRefund | boolean | Whether the purchase was refunded | | willRenew | boolean | Whether the auto-renewable subscription is set to renew | | activeIntroductoryOfferType | string (optional) | The type of active introductory offer. Possible values are: free_trial, pay_as_you_go, pay_up_front. If the value is not null, it means that the offer was applied during the current subscription period | | activePromotionalOfferType | string (optional) | The type of active promotional offer. Possible values are: free_trial, pay_as_you_go, pay_up_front. If the value is not null, it means that the offer was applied during the current subscription period | | activePromotionalOfferId | string (optional) | An identifier of active promotional offer | | offerId | string (optional) | An identifier of a discount offer in Google Play that unlocked this subscription | | cancellationReason | string (optional) | The reason why the subscription was cancelled. Possible values are: voluntarily_cancelled, billing_error, refund, price_increase, product_was_not_available, unknown | ### AdaptyProfile.NonSubscription Information about the user's non-subscription purchases. | Name | Type | Description | |----|----|-----------| | purchaseId | string | The identifier of the purchase in Adapty. You can use it to ensure that you've already processed this purchase (for example tracking one time products) | | store | string | The store of the purchase. The possible values are: play_store, adapty | | vendorProductId | string | The identifier of the product in the App Store Connect | | vendorTransactionId | string (optional) | Transaction id from the App Store | | purchasedAt | string (ISO 8601 datetime) | The time when the product was purchased | | isSandbox | boolean | Whether the product was purchased in the sandbox environment | | isRefund | boolean | Whether the purchase was refunded | | isConsumable | boolean | Whether the product should only be processed once. If true, the purchase will be returned by Adapty API one time only | ### AdaptyAndroidSubscriptionUpdateParameters Parameters to change one subscription to another. | Name | Type | Description | | :-------------------- | :----- | :----------------------------------------------------------- | | oldSubVendorProductId | string | The product id for current subscription to change | | replacementMode | [AdaptyAndroidSubscriptionUpdateReplacementMode](#adaptyandroidsubscriptionupdatereplacementmode) | The proration mode for subscription update | ### Enums #### ProductPeriod - `day` - Day period - `week` - Week period - `month` - Month period - `year` - Year period #### OfferType - `free_trial` - Free trial - `pay_as_you_go` - Pay as you go - `pay_up_front` - Pay up front #### AdaptyAndroidSubscriptionUpdateReplacementMode - `immediate_with_time_proration` - Immediate with time proration - `immediate_and_charge_prorated_price` - Immediate and charge prorated price - `immediate_without_proration` - Immediate without proration - `deferred` - Deferred - `immediate_and_charge_full_price` - Immediate and charge full price --- # File: android-sdk-overview.md --- --- title: "Android SDK overview" description: "Learn about Adapty Android SDK and its key features." slug: /android-sdk-overview displayed_sidebar: sdkandroid --- [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-Android.svg?style=flat&logo=android)](https://github.com/adaptyteam/AdaptySDK-Android/releases) Welcome! We're here to make in-app purchases a breeze 🚀 We've built the Adapty Android 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-android.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-android.md): Add the SDK as a dependency to your project and activate it in the code. 2. [Enable purchases through paywalls](android-quickstart-paywalls.md): Set up the purchase flow so users can buy products. 3. [Check the subscription status](android-check-subscription-status.md): Automatically check the user's subscription state and control their access to paid content. 4. [Identify users (optional)](android-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-Android) that demonstrates the full setup ## 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: android-setting-user-attributes.md --- --- 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 up to 30 characters | | 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-settings.md --- --- title: "Google Play Store credentials" description: "Configure Android settings in Adapty for seamless subscription management." --- For Adapty Android SDK to work, you need to configure several parameters. | Field | Description | | :------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Package name** | The Package name is the unique identifier of your app in the Google Play Store. This is required for the basic functionality of Adapty, such as subscription processing. | | **Service account key file** | [Keys](create-service-account) to enable secure authentication and validation of purchases. | | **Google Play RTDN topic name** | URL that is used to enable [server2server notifications](enable-real-time-developer-notifications-rtdn) from the Play Store to monitor and respond to users' subscription status changes. | --- # File: android-test.md --- --- title: "Test & release in Android SDK" description: "Learn how to check subscription status in your Android app with Adapty." displayed_sidebar: sdkandroid --- 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. For comprehensive testing of your in-app purchases, including sandbox testing and closed track validation, see our [testing guide](testing-on-android.md). --- # File: android-troubleshoot-paywall-builder.md --- --- 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-troubleshoot-purchases.md --- --- 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-use-fallback-paywalls.md --- --- title: "Android - Use fallback paywalls" description: "Use fallback paywalls in Android apps with Adapty to ensure revenue flow." --- To use fallback paywalls: 1. Place the fallback JSON file you [downloaded in the Adapty Dashboard](fallback-paywalls#download-fallback-paywalls-as-a-file-in-the-adapty-dashboard) in the `assets` or `res/raw` directory of your Android project. 2. Call the `.setFallback` method. Place this method in your code **before** fetching a paywall, ensuring that the mobile app possesses it when a fallback paywall is required to replace the standard one. Here's an example of retrieving fallback paywall data from locally stored JSON file named `android_fallback.json`. ```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://kotlin.adapty.io/adapty/com.adapty.utils/-file-location/-companion/) for the file with fallback paywalls | --- # File: android-user.md --- --- title: "Users & access in Android SDK" description: "Learn how to work with users and access levels in your Android app with Adapty SDK." displayed_sidebar: sdkandroid --- This page contains all guides for working with users and access levels in your Android app. Choose the topic you need: - **[Identify users](android-identifying-users)** - Learn how to identify users in your app - **[Update user data](android-setting-user-attributes)** - Set user attributes and profile data - **[Listen for subscription status changes](android-listen-subscription-changes)** - Monitor subscription changes in real-time - **[Kids Mode](kids-mode-android)** - Implement Kids Mode for your app --- # File: api-responses.md --- --- title: "Responses to server-side API requests" description: "" displayed_sidebar: APISidebar --- - [200: Success](ss-200) - [400: Bad request](ss-400) - [401: Unauthorized](ss-401) - [404: Not found](ss-404) --- # File: app-store-connection-configuration.md --- --- title: "Configure App Store integration" description: "Configure your App Store connection for seamless subscription tracking." --- This section describes how to establish the connection between the App Store and Adapty for your iOS app. This is required for us to be able to show subscription analytics and validate purchases. You can complete the integration during the initial onboarding or later in the **App Settings** within the Adapty Dashboard. Although you might have initially configured the integration of your mobile app and Adapty during onboarding, you can modify these settings later in the **App settings**. :::danger Configuration changes can be done safely during the Sandbox phase, until your mobile app goes live with Adapty SDK installed. Changes after the release can break the purchase flow in your app. ::: ## Step 1. Provide Bundle ID Bundle ID is the unique identifier of your app in the App Store. This is required for the basic functionality of Adapty, such as subscription processing. --- 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. 4. Paste the copied value to the **Bundle ID** field. ## Step 2. Provide Issuer ID and Key ID The **In-app purchase Issuer ID**, referred to as **Issuer ID** in App Store Connect, is a special ID that identifies the issuer who created the authentication token. The **In-App Purchase Key ID**, referred to as **Key ID** in App Store Connect, is a unique identifier associated with a cryptographic key you've generated in the [Generate In-App Purchase Key in App Store Connect](generate-in-app-purchase-key) section. 1. Open **App Store Connect**. Proceed to [**Users and Access** → **Integrations** → **In-App Purchase**](https://appstoreconnect.apple.com/access/integrations/api/subs) section. 2. In the **Active** list, find the key you've created in the [Generate In-App Purchase Key in App Store Connect](generate-in-app-purchase-key) section. 3. Copy **Issuer ID** and paste it to the **In-app purchase Issuer ID** field in the Adapty Dashboard. 4. Copy the **Key ID** and paste it to the **In-app purchase Key ID** field in the Adapty Dashboard. ## Step 3. Upload In-App Purchase Key file Upload the **In-App Purchase Key** file you've downloaded in the [Generate In-App Purchase Key in App Store Connect](generate-in-app-purchase-key) section into the **Private key (.p8 file)** field in the Adapty Dashboard. ## Step 4. For trials and special offers – set up promotional offers :::important This step is required if your app has trials or other promotional offers. ::: 1. Copy the same key ID you used in [Step 2](#step-2-provide-issuer-id-and-key-id) to the **Subscription key ID** field in the **App Store promotional offers** section. 2. Upload the same **In-App Purchase Key** file you used in [Step 3](#step-3-upload-in-app-purchase-key-file) to the **Subscription key (.p8 file)** area in the **App Store promotional offers** section. ## Step 5. Enter App Store shared secret The **App Store shared secret**, also known as the App Store Connect Shared Secret, is a 32-character hexadecimal string used for in-app purchases and subscription receipt validation. 1. Open [App Store Connect](https://appstoreconnect.apple.com/apps). Select your app and proceed to **General** → **App Information** section. 2. Scroll down to the **App-Specific Shared Secret** sub-section. :::info If the **App-Specific Shared Secret** sub-section is absent, make sure you have an Account Holder or Admin role. If you have an Admin role and yet cannot see the **App-Specific Shared Secret** sub-section, ask the Account Holder of the app (the person who has created the application in the App Store Connect) to generate the App Store shared secret for the app. After that, the sub-section will be shown to Admins as well. ::: 3. Click the **Manage** button. 4. In the opened **App-Specific Shared Secret** window, copy the **Shared Secret**. If no shared secret is visible, first click either the **Manage** or **Generate** button whichever is available, and then copy the **Shared Secret**. 5. Paste the copied **Shared Secret** to the **App Store shared secret** field in the Adapty Dashboard. 6. Click the **Save** button in the Adapty Dashboard to confirm the changes. **What's next** - [Enable App Store server notifications](enable-app-store-server-notifications) --- # File: app-store-offers.md --- --- title: "Offers in App Store" description: "Set up and manage App Store offers to increase user retention." --- Offers in the App Store are special deals or discounts provided by these platforms for in-app purchases. Developers use offers to provide users with exciting promotions, like discounted prices, free trials, or bundled offers. These promotions help attract and keep users engaged, making the app experience more rewarding. By using these special incentives, developers can boost user interest and loyalty, contributing to the overall success of their apps. With iOS 13 Apple released [Promotional offers](https://developer.apple.com/documentation/storekit/in-app_purchase/subscriptions_and_offers/implementing_promotional_offers_in_your_app) as a way to promote your users to subscribe. Adapty supports Subscription offers. :::note To use promotional offers, you have to [upload subscription key](app-store-connection-configuration#step-3-upload-in-app-purchase-key-file) to Adapty dashboard, so Adapty can sign the offers. ::: Please also consider that introductory offers on iOS are applied automatically if the user is eligible. To include a promotional offer or free trial for your product, navigate to the promotional offers tab after setting up the pricing. You will find a **+ icon** next to promotional offers, and click on it to begin the setup. In the subsequent modal, you'll encounter various configuration screens: 1. **Promotional offer reference name and promotional offer identifier**: This will configure the name and ID of the offer. 2. **Type of promotional offer**: You'll be able to choose the type of promotional offer from Pay as you go, Pay up front, and Free options. Then choose the desired Duration from the dropdown for the selected option. 3. **Prices for the offer for each country** You can check our [documentation](app-store-products) to learn how to configure the products in the App Store. --- # File: app-store-products.md --- --- title: "Product in App Store" description: "Manage App Store products efficiently using Adapty’s subscription tools." --- This page provides guidance on creating a product in App Store Connect. While this information may not directly pertain to Adapty's functionality, it serves as a valuable resource if you encounter challenges while creating products in your App Store Connect account. To create a product that will be linked to Adapty: 1. Open **App Store Connect**. Proceed to [**Monetization** → **Subscriptions**](https://appstoreconnect.apple.com/apps/6477523342/distribution/subscriptions) section in the left-side menu. 2. If you haven't created a subscription group, click the **Create** button under the **Subscription Groups** title to initiate the process. [Subscription Groups](https://developer.apple.com/help/app-store-connect/manage-subscriptions/offer-auto-renewable-subscriptions) in App Store Connect categorize and manage your products, allowing users to switch between different offerings seamlessly. Note that it's not possible to create a subscription outside of a group. 3. In the opened **Create Subscription Group** window, enter a the new subscription group name in the **Reference Name** field. The reference name is a user-defined label or identifier that helps you distinguish and manage different subscription groups within your app. The reference name is not visible to users; it's primarily for your internal use and organization. It allows you to easily identify and refer to specific subscription groups when managing them within the App Store Connect interface. This can be particularly useful if you have multiple subscription offerings or want to categorize them in a way that makes sense for your app's structure. 4. Click the **Create** button to confirm the subscription group creation. 5. The subscription group is created and opened. Now you can create subscriptions in the group. Click the **Create** button under the **Subscriptions** title. If you add a new subscription to an existing group, then click a **Plus** button next to the **Subscriptions** title. 6. In the opened **Create Subscription** window, enter its name in the **Reference Name** field and subscription unique code in the **Product ID** field. The Reference Name serves as an exclusive identifier within App Store Connect for your in-app subscription. It is not visible to your users on the App Store. We recommend using a clear, human-readable description that accurately represents the specific subscription you intend to create. Please note that this name must not exceed 64 characters in length. The Product ID is a unique alphanumeric identifier essential for accessing your product during the development phase and synchronizing it with Adapty, a service designed to manage in-app subscriptions. Only alphanumeric characters, periods, and underscores are allowed in the Product ID. 7. Click the **Create** button to confirm the subscription creation. 8. The subscription is created and opened. Now select the duration of the subscription in the **Subscription Duration** list. Even if the subscription duration is already indicated in the subscription name, remember to complete the **Subscription Duration** field. 9. Now it's time to set up the subscription price. To do so, click the **Add Subscription Price** button under the Subscription Prices title. You may need to scroll down to find them. 10. In the opened **Subscription Price** window, select the basic country in the **Country or Region** list and and basic currency in the **Price** list. Later Apple will automatically calculate the prices for all 175 countries or regions based on this basic price and the most recent foreign exchange rates. 11. Click the **Next** button. In the opened **Price by Country or Region** window, you see the automatically recalculated prices for all countries. You can change them if you want. 12. After updating regional prices, proceed by clicking the **Next** button at the bottom of the window. 13. In the opened **Confirm Subscription Price?** window, carefully review the final prices. To correct the prices, you can click the **Back** button to return to the **Price by Country or Region** window and update them. When you are ok with the prices, click the **Confirm** button. 14. After closing the **Confirm Subscription Price?** window, remember to click the **Save** button in your subscription window. Without it, the subscription won't be created, and all entered data will be lost. Please consider, that the steps provided so far focus on configuring an Auto-Renewable Subscription. However, if you intend to set up other types of in-app purchases, you can click on the **In-App Purchases** tab in the sidebar, instead of "Subscriptions." This will lead you to the section where you can manage and create various types of in-app purchases. ### Add products to Adapty Once you have completed adding your in-app purchases, subscriptions, and offers in App Store Connect, the next step is to [add these products to Adapty](create-product). --- # File: app-store-small-business-program.md --- --- title: "App Store Small Business Program" description: "Understand Apple’s Small Business Program and its impact on revenue." --- Learn about how Adapty calculates proceeds for both the App Store and Google Play Store, taking into account the reduced commission rate offered by the Small Business Program. Also, you can check the instructions on how to manage your Small Business Program membership status for the App Store in the Adapty Dashboard. By keeping your membership status up to date, you can ensure that Adapty accurately calculates your sales commission and provides reliable information on your transactions. Adapty also supports the reduced service fee program for Google Play. You can reference [this document](google-reduced-service-fee) for more details. ## App Store Small Business Program The App Store Small Business Program is a scheme that reduces the commission on App Store sales for eligible small businesses from 30% to 15%. The program is available to developers who earn less than $1 million in annual App Store revenue, subject to certain eligibility criteria. Further information about the program can be found on the App Store Small Business Program [page](https://developer.apple.com/app-store/small-business-program/). Acknowledging your membership in the Small Business Program in your app settings is essential, as the reduced commission rate will impact the data sent for integrations and the information displayed in Adapty's charts. By providing this information through Adapty, you can simplify the enrollment process and take advantage of the reduced commission rate offered by the program. To join the App Store Small Business Program, the first step is to visit the [Apple Developer website](https://developer.apple.com/app-store/small-business-program/). Before applying, please make sure that you are the Account Holder in the Apple Developer Program, have accepted the latest Paid Applications contract in App Store Connect, and can list all associated developer accounts to the account for which you are applying. After reviewing the program's terms, click the 'Enroll' button and sign in to your Apple Developer account. Apple's enrollment form will automatically fill in information like your name, email, and Team ID. After submitting your enrollment form, Apple will review your application. Once your enrollment is processed, you will receive a confirmation email that your enrollment is being reviewed. ## How Adapty calculates the proceeds App Store Adapty can accurately calculate your app's earnings by deducting Apple's and Google's commissions and taking into account your eligibility for the Small Business Program. In the Adapty Dashboard, the Small Business Program membership status is assigned to each individual app based on the developer's representation of multiple apps from different companies in their account. This means that the eligibility for the program is determined on a per-app basis. You can add multiple periods by selecting a range of dates for each period in the same field. The date range represents the start and end date of the period during which your business was a member of the Small Business Program. Please note that the Entry Date refers to the earliest date in the range when your business became a member of the program, and the Exit Date refers to the latest date in the range when your business officially left or was removed from the program. You can select your entry date according to your preference. However, it's important to note that if you select a past date, any webhooks and integration events already processed will not be resent with corrected pricing data. To ensure the accuracy of pricing data sent to your integrations, it's advisable to set your effective entry date as soon as possible. This way, you can receive reliable and up-to-date information on your transactions and make informed decisions accordingly. ## Letting Adapty know To manage your Small Business Program membership status for the App Store, go to the [**App Settings > General tab**](https://app.adapty.io/account) in your Adapty account. Click the **Add period** button to specify your membership status for a specific period range. In the "Period" field, select a date range that indicates your business's membership start and end dates. This range can include any date in the past or the future. You can add additional membership periods by clicking on the "Add Period" button again. You can select your period start according to your preference. However, if you select a past period start, any webhooks and integration events already processed will not be resent with corrected pricing data. To ensure the accuracy of pricing data sent to your integrations, it's advisable to set your effective period start as soon as possible. This way, you can receive reliable and up-to-date information on your transactions and make informed decisions accordingly. Please note that the Small Business Program membership status will only apply to the specific period range you've specified. Once the period end is reached, you'll need to add another period if you want to continue with the Small Business Program membership status. To ensure that we calculate your sales commission correctly, please enter the effective exit date in your Adapty Dashboard app settings as soon as possible if your business has left the Small Business Program. If no exit date is provided, we will continue to calculate your commission based on the reduced rate. --- # File: apple-app-privacy.md --- --- title: "Apple app privacy" description: "Understand Apple app privacy policies and their impact on your subscription app." --- Apple requires a privacy disclosure for all new apps and app updates both in the [**App Privacy**](https://appstoreconnect.apple.com/apps/6477523342/distribution/privacy) section of the App Store Connect and as the app manifest file. Adapty is a third-party dependency to your app, therefore you’ll need to properly disclose the ways you are using Adapty in regards to user's data. ## Apple app privacy manifest The [privacy manifest file](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests), named `PrivacyInfo.xcprivacy`, describes what private data your app uses and why. You as every app owner must create a manifest file for your app. Additionally, if you're integrating any extra SDKs, ensure the manifest files for those of them included in the [SDKs that require a privacy manifest and signature](https://developer.apple.com/support/third-party-SDK-requirements/) list are included. When you build your app, Xcode will take all these manifest files and merge them into one. Even though Adapty isn't on the list of [SDKs that require a privacy manifest and signature](https://developer.apple.com/support/third-party-SDK-requirements/), versions 2.10.2 and higher of the Adapty SDK include it for your convenience. Make sure to update the SDK to get the manifest. While Adapty doesn't require any data to be included in the manifest file also called app privacy report, if you're using Adapty's ` customerUserId` for tracking, it's necessary to specify it in your manifest file like so: 1. Add a dictionary to the `NSPrivacyCollectedDataTypes` array in your privacy information file. 2. Add the `NSPrivacyCollectedDataType`, `NSPrivacyCollectedDataTypeLinked`, and `NSPrivacyCollectedDataTypeTracking` keys to the dictionary. 3. Add string `NSPrivacyCollectedDataTypeUserID` (identifier of the `UserID` data type in the [List of data categories and types to be reported in the manifest file](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests#4250555)) for the `NSPrivacyCollectedDataType` key in your `NSPrivacyCollectedDataTypes` dictionary. 4. Add `true` for the `NSPrivacyCollectedDataTypeTracking` and `NSPrivacyCollectedDataTypeLinked` keys in your `NSPrivacyCollectedDataTypes` dictionary. 5. Use the `NSPrivacyCollectedDataTypePurposeProductPersonalization` string as the value for the `NSPrivacyCollectedDataTypePurposes` key in your `NSPrivacyCollectedDataTypes` dictionary. If you target your paywalls to audiences with custom attributes, consider carefully what custom attributes you use and if they match the [data categories and types to be reported in the manifest file](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests#4250555). If so, repeat the steps above for every data type. After you report all data types and categories you collect, create your app's privacy report as described in [Apple documentation](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests#4239187). ## Apple app privacy disclosure in App Store Connect In the [**App Privacy**](https://appstoreconnect.apple.com/apps/6477523342/distribution/privacy) section of the App Store Connect, make sure to clearly explain how you're using Adapty in relation to user data. ### Data types ✅ = Required 👀 = May be required \(see details below\) ❌ = Not required | Data type | Required | Note | |---------|--------|----| | Identifiers | ✅ |

If you are identifying users with a customerUserId, select 'User ID'.

Adapty collects IDFA, so you have to select 'Device ID'.

| | Purchases | ✅ | Adapty collects purchase history from users. | | Contact Info, including name, phone number, or email address | 👀 | Required if you pass personal data like name, phone number, or email address using **`updateProfile`** method. | | Usage Data | 👀 | If you are using analytics SDKs such as Amplitude, Mixpanel, AppMetrica, or Firebase, this may be required. | | Location | ❌ | Adapty does not collect precise location data. | | Health & Fitness | ❌ | Adapty does not collect health or fitness data from users. | | Sensitive Info | ❌ | Adapty does not collect sensitive information. | | User Content | ❌ | Adapty does not collect content from users. | | Diagnostics | ❌ | Adapty does not collect device diagnostic information. | | Browsing History | ❌ | Adapty does not collect browsing history from users. | | Search History | ❌ | Adapty does not collect search history from users. | | Contacts | ❌ | Adapty does not collect contact lists from users. | | Financial Info | ❌ | Adapty does not collect financial info from users. | ### Required data types #### Purchases When using Adapty, you must disclose that your app collects ‘Purchases’ information. #### Identifiers If you are identifying users with **`customerUserId`**, you'll need to select 'User ID'. Adapty collects IDFA, so you'll need to select 'Device ID'. After making your selections, you'll need to indicate how the data is used similar to the Purchases section. After making your privacy selections, Apple will show a preview of your app's privacy section. If you have chosen Purchases and Identifiers as described above, your app's privacy details should look something like this: --- # File: apple-family-sharing.md --- --- title: "Apple family sharing" description: "Enable Apple Family Sharing in Adapty to support shared subscriptions." --- Apple's family sharing enables the distribution of in-app purchases among family members, offering users of group-oriented apps, such as video streaming services and kids' apps, a convenient way to split subscriptions without having to share their Apple ID. By allowing up to five family members to utilize a subscription, [Family sharing](https://developer.apple.com/documentation/storekit/in-app_purchase/original_api_for_in-app_purchase/supporting_family_sharing_in_your_app) can potentially improve customer engagement and retention for your app. In this guide, we will provide instructions on how to opt-in subscriptions to Family Sharing and explain how Adapty manages purchases that are shared within a family. To get started with enabling Family Sharing for a particular product, head over to [App Store Connect](https://appstoreconnect.apple.com/). Family Sharing is turned off by default for both new and existing in-app purchases, so it is necessary to enable it individually for each in-app purchase. You can easily do this by accessing your **app's page,** navigating to the corresponding in-app purchase page, and selecting the **Turn On** option in the Family Sharing section. Keep in mind that once you enable Family Sharing for a product, **it cannot be turned off again**, as this would disrupt the user experience for those who have already shared the subscription with their family members. Also, please consider that, only non-consumables and subscriptions can be shared. On the displayed modal simply click on the **Confirm** button to finalize the setup process. After doing so, the Family Sharing section should update to display the message, "This subscription can be shared by everyone in a family group." This confirms that the subscription is now enabled for Family Sharing and can be shared among up to five family members. Adapty makes it easy to support Family Sharing without any additional effort required. You just need to simply [configure your products](app-store-products) from the App Store, and once you **enable** it from App Store Connect **Family Sharing** will be automatically available in **Adapty**, that will be received as an event on the webhook. :::note Please note that Family Sharing is not supported in the sandbox environment. ::: One thing you can consider is that when a user purchases a subscription and shares it with their family members, there is a **delay of up to one hour** before it becomes available to them. Apple designed this delay to give the user time to change their mind and undo the sharing if they want to. However, if the subscription is renewed, there is no delay in making it available to the family members. When a user purchases a Family Shareable in-app product, the transaction will appear in their receipt as usual, but with the addition of a new field called `in_app_ownership_type` with the value `PURCHASED.` Furthermore, a new transaction will be created for all family members, which will have a different `web_order_line_item_id` and `original_transaction_id` compared to the original purchase, as well as an `in_app_ownership_type` field with the value `FAMILY_SHARED.` To ensure accurate revenue calculation, on the Adapty side, only transactions with an `in_app_ownership_type` value of `PURCHASED` are considered. This means that we don't take into account `FAMILY_SHARED` values in analytics and do not send events based on them. To identify the other family members on Adapty, you can find them in the event details. First, locate the original family purchase transaction. Then, examine the event details for that transaction, specifically looking for the same product, purchase date, and expiration date. By analyzing the event details, you can identify other family membership transactions associated with the original purchase. --- # File: apple-platform-resources.md --- --- title: "Apple Platform resources" description: "Explore Apple platform resources to optimize your app’s monetization and subscription management." --- Adapty offers SDKs and integrations tailored for Apple Platforms, simplifying the development of in-app purchases, subscriptions, paywalls, and A/B tests. Use the following resources to maximize the benefits Adapty provides for Google Platforms. ### Initial configuration in Google Play Console 1. [Generate In-App Purchase Key in App Store Connect](generate-in-app-purchase-key) ### Products and offers configuration in Google Play Console 1. [Product in App Store](app-store-products) 2. [Offers in App Store](app-store-offers) ### Additional information 1. [Apple app privacy](apple-app-privacy) 2. [Apple family sharing](apple-family-sharing) 3. [App Store Small Business Program](app-store-small-business-program) --- # File: apple-search-ads.md --- --- title: "Apple Search Ads (ASA)" description: "Integrate Apple Search Ads with Adapty to optimize subscription conversions." --- Adapty can help you get attribution data from Apple Search Ads and analyze your metrics with campaign and keyword segmentation. Adapty collects the attribution data for Apple Search Ads automatically through its SDK and AdServices Framework. Once you've set up the Apple Search Ads integration, Adapty will start receiving attribution data from Apple Search Ads. You can easily access and view this data on the profiles page. There are two ways to get attribution: with the old iAd framework and the modern AdServices framework (iOS 14.3+). ## AdServices framework Apple Search Ads via [AdServices](https://developer.apple.com/documentation/ad_services) does require some configuration in Adapty Dashboard, and you will also need to enable it on the app side. To set up Apple Search Ads using the AdServices framework through Adapty, follow these steps: ### Step 1: Configure Info.plist Add `AdaptyAppleSearchAdsAttributionCollectionEnabled` to the app’s `Info.plist` file and set it to `YES` (boolean value). ### Step 2: Obtain Public Key In the Adapty Dashboard, navigate to [Settings -> Apple Search Ads.](https://app.adapty.io/settings/apple-search-ads) Locate the pre-generated public key (Adapty provides a key pair for you) and copy it. :::note If you're using an alternative service or your own solution for Apple Search Ads attribution, you can upload your own private key. ::: ### Step 3: Configure User Management on Apple Search Ads In your [Apple Search Ads account](https://searchads.apple.com/) go to Settings > User Management page. In order for Adapty to fetch attribution data you need to invite another Apple ID account and grant it API Account Manager access. ### Step 4: Generate API Credentials As a next step, log in to the newly added account in Apple Search Ads. Navigate to Settings -> API in the Apple Search Ads interface. Paste the previously copied public key into the designated field. Generate new API credentials. ### Step 5: Configure Adapty with Apple Search Ads Credentials Copy the Client ID, Team ID, and Key ID fields from the Apple Search Ads settings. In the Adapty Dashboard, paste these credentials into the corresponding fields. ## Uploading your own keys :::note Optional These steps are not required for Apple Search Ads attribution, only for working with other services like Asapty or your own solution. ::: You can use your own public-private key pair if you are using other services or own solution for ASA attribution. ### Step 1 Generate private key in Terminal ```text showLineNumbers title="Text" openssl ecparam -genkey -name prime256v1 -noout -out private-key.pem ``` Upload it in Adapty Settings -> Apple Search Ads (Upload private key button) ### Step 2 Generate public key in Terminal ```text showLineNumbers title="Text" openssl ec -in private-key.pem -pubout -out public-key.pem ``` You can use this public key in your Apple Search Ads settings of account with API Account Manager role. So you can use generated Client ID, Team ID, and Key ID values for Adapty and other services. ## Disabling Apple Search Ads attribution Adapty can use attribution data in analytics from only one source at a time. If multiple attribution sources are enabled, the system will decide which attribution to use for each device based on the source that provides more fields. For iOS devices, this means non-organic Apple Search Ads attribution will always take priority if it's enabled. You can disable Apple Search Ads attribution receiving by toggling off the **Receive Apple Search Ads attribution in Adapty** in the [**App Settings** -> **Apple Search Ads** tab](https://app.adapty.io/settings/apple-search-ads). :::warning Please note that disabling this will completely stop the reception of ASA analytics. As a result, ASA will no longer be used in analytics or sent to integrations. Additionally, SplitMetrics Acquire and Asapty will cease to function, as they rely on ASA attribution to operate correctly. The attribution received before this change will not be affected. ::: --- # File: appmetrica.md --- --- title: "AppMetrica" description: "Integrate AppMetrica with Adapty for in-depth subscription analytics." --- [AppMetrica](https://appmetrica.yandex.ru/en/about) is a free analytics tool that helps you track user behavior and analyze your mobile app's performance in real time. By integrating AppMetrica with Adapty, you can gain deeper insights into your subscription metrics and user engagement. ## How to set up AppMetrica integration Setting up the AppMetrica integration involves two main steps: 1. Configure the integration in the Adapty Dashboard 2. Set up the integration in your app's code ### Dashboard configuration To set up the AppMetrica integration: 1. Open the [AppMetrica apps list](https://appmetrica.yandex.ru/application/list) 2. Select the app you want to track 3. Go to **Settings** and copy the **Application ID** and **Post API key** 4. Go to [Integrations > AppMetrica](https://app.adapty.io/integrations/appmetrica) in the Adapty Dashboard 5. Paste your AppMetrica credentials. ### Events and tags Adapty allows you to send three groups of events to AppMetrica. You can enable the events you need to track your app's performance. For a complete list of available events, see our [events documentation](events). :::note AppMetrica syncs events every 4 hours, so there may be a delay before events appear in your dashboard. ::: :::tip We recommend using Adapty's default event names for consistency, but you can customize them to match your existing analytics setup. ::: ### Revenue settings By default, Adapty sends revenue data as properties in events, which appear in AppMetrica's Events report. You can configure how this revenue data is calculated and displayed: - **Revenue calculation**: Choose how revenue values are calculated to match your financial reporting needs: - **Gross revenue**: Shows the total revenue before any deductions, useful for tracking the full amount customers pay - **Proceeds after store commission**: Displays revenue after App Store/Play Store fees are deducted, helping you track actual earnings - **Proceeds after store commission and taxes**: Shows net revenue after both store fees and applicable taxes, providing the most accurate picture of your earnings - **Report user's currency**: When enabled, sales are reported in the user's local currency, making it easier to analyze revenue by region. When disabled, all sales are converted to USD for consistent reporting across different markets. - **Send revenue events**: Enable this option to make revenue data appear not only in the Events report but also in AppMetrica's [In-app and ad revenue](https://appmetrica.yandex.com/docs/en/mobile-reports/revenue-report) report. Make sure you’re not sending revenue from anywhere else, as this may result in duplication. - **Exclude historical events**: When enabled, Adapty won't send events that occurred before the user installed the app with Adapty SDK. This helps avoid data duplication if you were already sending events to analytics before integrating Adapty. ### SDK configuration To enable the AppMetrica integration in your app, you need to set up two identifiers: 1. `appmetrica_device_id`: Required for basic integration 2. `appmetrica_profile_id`: Optional, but recommended if your app has user registration Use the `setIntegrationIdentifier()` method to set these values. Here's how to implement it for each platform: **Setting appmetrica_device_id** ```swift showLineNumbers AppMetrica.requestStartupIdentifiers(on: nil) { ids, error in if let error { // handle AppMetrica error return } guard let deviceIDHash = ids?[.deviceIDHashKey] as? String else { // handle AppMetrica error return } Task { do { try await Adapty.setIntegrationIdentifier( key: "appmetrica_device_id", value: deviceIDHash ) } catch { // handle the error } } } ``` **Setting appmetrica_profile_id** ```swift showLineNumbers do { try await Adapty.setIntegrationIdentifier( key: "appmetrica_profile_id", value: "YOUR_APPMETRICA_PROFILE_ID" ) } catch { // handle the error } ``` **Setting appmetrica_device_id** ```kotlin showLineNumbers val startupParamsCallback = object: StartupParamsCallback { override fun onReceive(result: StartupParamsCallback.Result?) { val deviceIdHash = result?.deviceIdHash ?: return Adapty.setIntegrationIdentifier("appmetrica_device_id", deviceIdHash) { 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_HASH)) ``` **Setting appmetrica_profile_id** ```kotlin showLineNumbers val startupParamsCallback = object: StartupParamsCallback { override fun onReceive(result: StartupParamsCallback.Result?) { val deviceIdHash = result?.deviceIdHash ?: return Adapty.setIntegrationIdentifier("appmetrica_device_id", deviceIdHash) { 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_HASH)) ``` **Setting appmetrica_device_id** ```javascript showLineNumbers final startupParams = await AppMetrica.requestStartupParams([AppMetricaStartupParams.deviceIdHashKey]); final deviceIdHash = startupParams.result?.deviceIdHash; if (deviceIdHash != null) { try { await Adapty().setIntegrationIdentifier( key: "appmetrica_device_id", value: deviceIdHash, ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } } ``` **Setting appmetrica_profile_id** ```javascript showLineNumbers try { await Adapty().setIntegrationIdentifier( key: "appmetrica_profile_id", value: "YOUR_APPMETRICA_PROFILE_ID", ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` **Setting appmetrica_device_id** ```csharp showLineNumbers using AdaptySDK; using Io.AppMetrica; AppMetrica.RequestStartupParams( (result, errorReason) => { string deviceIdHash = result.DeviceIdHash; if (deviceIdHash != null) { Adapty.SetIntegrationIdentifier( "appmetrica_device_id", deviceIdHash, (error) => { // handle the error }); } }, new List() { StartupParamsKey.AppMetricaDeviceIDHash } ); ``` **Setting appmetrica_profile_id** ```csharp showLineNumbers Adapty.SetIntegrationIdentifier( "appmetrica_profile_id", "YOUR_APPMETRICA_PROFILE_ID", (error) => { // handle the error }); ``` **Setting appmetrica_device_id** ```typescript showLineNumbers // ... const startupParamsCallback = async ( params?: StartupParams, reason?: StartupParamsReason ) => { const deviceIdHash = params?.deviceIdHash if (deviceIdHash) { try { await adapty.setIntegrationIdentifier("appmetrica_device_id", deviceIdHash); } catch (error) { // handle `AdaptyError` } } } AppMetrica.requestStartupParams(startupParamsCallback, [DEVICE_ID_HASH_KEY]) ``` **Setting appmetrica_profile_id** ```typescript showLineNumbers try { await adapty.setIntegrationIdentifier("appmetrica_profile_id", 'YOUR_ADAPTY_CUSTOMER_USER_ID'); } catch (error) { // handle `AdaptyError` } ``` --- # File: appsflyer.md --- --- title: "AppsFlyer" description: "Integrate AppsFlyer with Adapty for advanced mobile attribution tracking." --- [AppsFlyer](https://www.appsflyer.com/) is a leading platform for mobile attribution and marketing analytics. It stands as a third-party service that gathers and organizes data from marketing campaigns. This helps companies see how well their campaigns are performing in one place. Adapty provides a complete set of data that lets you track [subscription events](events) from stores in one place. With Adapty, you can easily see how your subscribers are behaving, learn what they like, and use that information to communicate with them in a way that's targeted and effective. Therefore, this integration allows you to track subscription events in AppsFlyer and analyze precisely how much revenue your campaigns generate. The integration between Adapty and AppsFlyer operates in two main ways. 1. **Receiving attribution data from AppsFlyer** Once you've [set up sending Appsflyer attribution to Adapty in your app code](appsflyer#sdk-configuration), Adapty will start receiving attribution data from AppsFlyer. You can easily access and view this data on the user's profile page. 2. **Sending subscription events to AppsFlyer** Adapty can send all subscription events that are configured in your integration to AppsFlyer. As a result, you'll be able to track these events within the AppsFlyer dashboard. This integration is beneficial for evaluating the effectiveness of your advertising campaigns. ## How to set up AppsFlyer integration To setup the integration with AppsFlyer: 1. Open [**Integrations** -> **AppsFlyer**](https://app.adapty.io/integrations/appsflyer) in the Adapty Dashboard. 2. Turn on the toggle to enable the integration. 3. The next step of the integration is to set credentials. For iOS, find App ID in the copy **Apple ID** in the App Store Connect (to do it, open your app page in [App Store Connect](https://appstoreconnect.apple.com/), go to the **App Information** page in **General** section, and find **Apple ID** in the left bottom part of the screen). 3.2. Paste the copied **Apple ID** to the **iOS App ID** in the Adapty Dashboard. :::warning If you use AppsFlyer API 2, you need to switch to API 3, since the previous version will be deprecated by AppsFlyer soon. To do so, in the **AppsFlyer S2S API** list, select **API 3**. ::: 5. For both iOS and Android, open the [AppsFlyer site](https://appsflyer.com/home) and log in. 6. Click **Your account name** -> **Security Center** in the top-right corner of the dashboard. 7. In the **Manage your account security** window, click the **Manage your AppsFlyer API and S2S tokens** button. 8. If you have an S2S token, please proceed to step 12. If you do not have it, click the **New token** button. 9. In the **New token** window, enter the name of the token. This name is solely for your reference. 10. Choose **S2S** in the **Choose type** list. 11. Click the **Create new token** button to save the new token. 12. In the **Tokens** window, copy the S2S token. 13. In the Adapty Dashboard, paste the copied S2S key into the **Dev key for iOS** and **Dev key for Android** fields. 14. Click the **Save** button to save the changes. :::info AppsFlyer doesn't have a Sandbox mode for server2server integration. So you need a different application/account in AppsFlyer for Sandbox Dev Key. If you want to send sandbox events to the same app, just use the same key for production and sandbox. ::: Adapty maps some events to AppsFlyer [standard events](https://support.appsflyer.com/hc/en-us/articles/115005544169-Rich-in-app-events-for-Android-and-iOS#event-types) by default. With such a configuration, AppsFlyer can then forward events to each ad network that you use without additional setup. Another important thing is that AppsFlyer doesn't support events older than 24 hours. So, if you have an event that is more than a day old, Adapty will send it to Appsflyer, but the event date and time will be replaced by the current timestamp. ## Events and tags Below the credentials, there are three groups of events you can send to AppsFlyer from Adapty. Simply turn on the ones you need. Check the full list of the events offered by Adapty [here](events). We recommend using the default event names provided by Adapty. But you can change the event names based on your needs. Adapty will send subscription events to AppsFlyer using a server-to-server integration, allowing you to view all subscription events in your AppsFlyer dashboard and link them to your acquisition campaigns. ## SDK configuration It's very important to send AppsFlyer attribution data from the device to Adapty using the `Adapty.updateAttribution()` SDK method and the `Adapty.setIntegrationIdentifier()` method to set the integration identifier. The example below shows how to do that. ```swift showLineNumbers class YourAppsFlyerLibDelegateImplementation { // Find your implementation of AppsFlyerLibDelegate // and update onConversionDataSuccess method: func onConversionDataSuccess(_ conversionInfo: [AnyHashable : Any]) { let uid = AppsFlyerLib.shared().getAppsFlyerUID() Adapty.setIntegrationIdentifier(key: "appsflyer_id", value: uid) Adapty.updateAttribution(conversionInfo, source: "appsflyer") } } ``` ```kotlin showLineNumbers val conversionListener: AppsFlyerConversionListener = object : AppsFlyerConversionListener { override fun onConversionDataSuccess(conversionData: Map) { 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 } } } } ``` ```javascript showLineNumbers AppsflyerSdk appsflyerSdk = AppsflyerSdk(); appsflyerSdk.onInstallConversionData((data) async { try { final appsFlyerUID = await appsFlyerSdk.getAppsFlyerUID(); 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, ); ``` ```csharp showLineNumbers using AdaptySDK; using AppsFlyerSDK; // before SDK initialization AppsFlyer.getConversionData(this.name); // in your IAppsFlyerConversionData void onConversionDataSuccess(string conversionData) { string appsFlyerId = AppsFlyer.getAppsFlyerId(); Adapty.SetIntegrationIdentifier( "appsflyer_id", appsFlyerId, (error) => { // handle the error }); Adapty.UpdateAttribution( conversionData, "appsflyer", (error) => { // handle the error }); } ``` ```typescript showLineNumbers appsFlyer.onInstallConversionData(installData => { try { const uid = appsFlyer.getAppsFlyerUID(); adapty.setIntegrationIdentifier("appsflyer_id", uid); adapty.updateAttribution(installData, "appsflyer"); } catch (error) { // handle the error } }); // ... appsFlyer.initSdk(/*...*/); ``` --- # File: archive-paywalls.md --- --- title: "Archive paywall" description: "Learn how to archive outdated paywalls in Adapty without losing data." --- As you dive into Adapty and fine-tune your paywall settings, you might accumulate paywalls that no longer fit your current strategy or campaigns. These unused paywalls, left `Inactive`, can clutter your workspace, making it tricky to find the ones that matter most. To tackle this, Adapty introduces the option to archive these unnecessary paywalls. Archiving ensures they're safely stored without permanent deletion, ready to be accessed if needed in the future. Plus, archived paywalls can be filtered out from the default view, decluttering your workspace and simplifying your user interface. In this guide, we'll walk you through efficiently archiving paywalls in Adapty, giving you greater control over your paywall management process. Just a friendly reminder: Live paywalls that are currently active in at least one placement cannot be archived. If you wish to archive such a paywall, simply remove it from all placements beforehand. :::note You can't archive a paywall if it is used in a non-archived A/B test. This way, the user can view detailed metrics for a completed A/B test, and the linked paywall is part of that data. ::: **To archive a paywall:** 1. Open the [**Paywalls**](https://app.adapty.io/paywalls) section in the Adapty main menu. 2. Click the **3-dot** button next to the paywall and select the **Archive** option. 3. When you're in the **Archive paywall** window, simply type in the name of the paywall you wish to archive and then click the **Archive** button. --- # File: arppu.md --- --- title: "ARPPU" description: "Understand ARPPU (Average Revenue Per Paying User) and how it impacts your app’s monetization." --- ### ARPPU The Average revenue per paying user (ARPPU) chart displays the average revenue per paid user. It displays the actual revenue generated by paying customers, divided by the number of customers, minus refunds. ### Calculation The ARPPU chart is calculated as: ARPPU = Revenue / Number of users who paid For example, if your app generated $10,000 in revenue over a given period and had 500 paying customers during that time, the ARPPU would be calculated as $10,000 / 500 = $20. This means that on average, each paying customer generated $20 in revenue during the selected period. It's important to note that the ARPPU value displayed represents the sum of total revenue divided by the total number of paying customers. The calculation is done before the store's fee and the refund amount is excluded from the revenue. As one user can pay more than one time during the whole period, the total ARPPU value may be higher than the daily value of ARPPU. This metric provides valuable insights into the revenue-generating capabilities of your app's paying user base and can help you optimize your pricing and subscription strategy to maximize revenue generation. ### Available filters and grouping - ✅ Filter by: Attribution, country, paywall, store, product, and duration. - ✅ Group by: Renewal status, period, product, country, store, paywall, duration, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative. You can find more information about the available controls, filters, grouping options, tax and commission controls, and how to use them in [this documentation.](controls-filters-grouping-compare-proceeds) ### ARPPU chart usage The ARPPU chart in Adapty is a powerful tool that can help businesses understand the revenue generated by each paying user. By analyzing this metric over time, businesses can identify which channels, networks, and campaigns attract the most valuable customers. This information can be used to optimize marketing strategies and increase revenue by targeting high-value customers. With Adapty's ARPPU chart, businesses can make data-driven decisions that improve their bottom line and drive long-term success. ### Similar metrics In addition to the ARPPU chart, Adapty also provides metrics for other revenue-related events, such as Revenue, MRR, ARR, and ARPU. To learn more about these revenue-related metrics, please refer to the following documentation guides: - [Revenue](revenue) - [MRR](mrr) - [ARPU](arpu) - [ARR](arr) --- # File: arpu.md --- --- title: "ARPU" description: "Analyze Average Revenue Per User (ARPU) to optimize revenue generation." --- ARPU (average revenue per user) chart displays the average revenue generated per user for a given period. This metric is calculated by dividing the total revenue generated by a cohort of customers by the number of users in that cohort. ### Calculation Adapty calculates the ARPU chart by dividing the total revenue earned over a given period by the number of non-unique users (installs) during the same period. ARPU = Revenue / Number of new users For instance, if your app generated $10,000 in revenue over the course of a week, and had 2,000 non-unique users during that same period, the ARPU for that week would be $5 ($10,000/2,000). The calculation is done before the store's fee and the refund amount is excluded from the revenue. ### Available filters and grouping - ✅ Filter by: Attribution, country, and store. - ✅ Group by: Country, store, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative. You can find more information about the available controls, filters, grouping options, tax and commission controls, and how to use them in [this documentation.](controls-filters-grouping-compare-proceeds) ### ARPU chart usage ARPU chart usage is beneficial for businesses to track their overall revenue generation and understand how much revenue is being generated per user. By analyzing the ARPU chart, businesses can identify trends, patterns, and areas of improvement to optimize their revenue generation strategies. This can help businesses make data-driven decisions to increase their user engagement, target their marketing efforts, and improve their overall monetization strategy. ### Similar metrics In addition to the ARPU chart, Adapty also provides metrics for other revenue-related events, such as Revenue, MRR, ARR, and ARPU. To learn more about these revenue-related metrics, please refer to the following documentation guides: - [Revenue](revenue) - [MRR](mrr) - [ARPPU](arppu) - [ARR](arr) --- # File: arr.md --- --- title: "ARR" description: "Track Annual Recurring Revenue (ARR) and optimize your subscription strategy." --- The Annual recurring revenue chart shows revenue from all active auto-renewable subscriptions normalized to one year. The chart considers any paid, unexpired subscription as active. ARR is a crucial metric for tracking your subscription business's growth and predicting future revenue. ### Calculation The Adapty ARR chart calculates the total revenue generated by the app. Any paid, unexpired subscription is considered active. ARR includes normalized annual revenue from all active paid subscriptions – even if their auto-renew status is currently disabled. This also means non-recurring subscriptions, consumable, or one-time purchases included in ARR calculation. The metric is calculated before the store's fee. ARR = sum of ( (Ps \* Ns / Dsy) where Ps - subscription price, Ns - number of active paid subscriptions for this subscription, Dsy - subscription duration in years (1/12 for monthly and ~1/52 for weekly subscriptions). This metric is only useful when annual subscriptions are the biggest chunk of your sales. For example, there are 2 active annual subscriptions with a price of $240, and 10 monthly subscriptions with a price of $30, and 20 weekly subscriptions with a price of $10, ARR = (2 _ $240 / 1) + (10 _ $30 / (1/12)) + (20\*$10 / (1/52)) = $14480 ### Available filters and grouping - ✅ Filter by: Attribution, country, paywall, store, product, and duration. - ✅ Group by: Renewal status, period, product, country, store, paywall, duration, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative. You can find more information about the available controls, filters, grouping options, tax and commission controls, and how to use them in [this documentation.](controls-filters-grouping-compare-proceeds) ### ARR chart usage The Annual Recurring Revenue (ARR) chart is a valuable tool for measuring the growth and scale of your subscription-based business. It's a widely-used metric that provides a normalized view of your recurring revenue over a 12-month period. To gain a better understanding of what's driving your ARR, you can segment your data by key subscriber segments such as Store or Product Duration. By doing so, you can identify which segments are driving the most revenue, and optimize your business strategy accordingly. ### Similar metrics In addition to the ARR chart, Adapty also provides metrics for other revenue-related events, such as Revenue, MRR, ARPU, and ARPPU. To learn more about these revenue-related metrics, please refer to the following documentation guides: - [Revenue](revenue) - [MRR](mrr) - [ARPU](arpu) - [ARPPU](arppu) --- # File: asapty.md --- --- title: "Asapty" description: "Discover Asapty and its role in Adapty’s subscription ecosystem." --- Using [Asapty](https://asapty.com/) integration you can optimize your Search Ads campaigns. Adapty sends subscription events to Asapty, so you can build custom dashboards there, based on Apple Search Ads attribution. This specific integration doesn't add any attribution data to Adapty, as we already have everything we need from [ASA](apple-search-ads) directly. ## How to set up Asapty integration To integrate Asapty navigate to [Integrations > Asapty](https://app.adapty.io/integrations/asapty) in the Adapty dashboard and fill out the field value for Asapty ID. Asapty ID can be found in Settings> General section in your Asapty account. ## Events and tags Below the credentials, there are three groups of events you can send to Asapty from Adapty. Simply turn on the ones you need. Check the full list of the events offered by Adapty [here](events). We recommend using the default event names provided by Asapty. But you can change the event names based on your needs. ## SDK configuration You don't have to configure anything on the SDK side, but we recommend sending `customerUserId` to Adapty for better accuracy. :::warning Troubleshooting - Make sure you've configured [Apple Search Ads](apple-search-ads) in Adapty and [uploaded credentials](https://app.adapty.io/settings/apple-search-ads), without them, Asapty won't work. - Only the profiles with detailed, non-organic ASA attribution will deliver their events to Asapty. You will see "The user profile is missing the required integration data." if the attribution is not sufficient. - Profiles created prior to configuring the integrations will not be able to deliver their events to Asapty. ::: ## Troubleshooting If the integration with Adapty isn't working despite the correct setup, ensure the **Receive Apple Search Ads attribution in Adapty** toggle is enabled in the [**App Settings** -> **Apple Search Ads** tab](https://app.adapty.io/settings/apple-search-ads). --- # File: assigning-access-level-to-a-product.md --- --- title: "Assign access level to product" description: "Assign access levels to products to optimize subscription management." --- Every [Product](product) requires an associated access level to ensure that users receive the corresponding gated content upon purchase. Adapty seamlessly determines the subscription duration, which then serves as the expiration date for the access level. In the case of a lifetime product, if a customer purchases it, the access level remains perpetually active without any expiration date. To link an access level to a product: 1. While [configuring a product](create-product), select the access level from the **Access Level ID** list. 2. Click the **Save** button to confirm the change. --- # File: attribution-integration.md --- --- title: "Attribution integration" description: "Integrate Adapty with attribution tools to track user acquisition and LTV." --- Adapty allows easy integration with the popular attribution services: [AppsFlyer](appsflyer), [Adjust](adjust), [Branch](branch), [Apple Search Ads](apple-search-ads), and [Facebook Ads](facebook-ads). Adapty will send [subscription events](events) to these services so you can accurately measure the performance of ad campaigns. You can also filter [charts data](analytics-charts) using attribution data. You can also integrate with [Adapty's User Acquisition](user-acquisition.md) to connect ad spend with subscription revenue, giving you a complete view of your app's economy in one place. ### Important Send subscription events with correct user properties and ID's to attributions services you use. :::warning - **Avoid event duplication**: Be sure to disable subscription event forwarding from both devices and your server to prevent duplicates. If you're using direct integration with Facebook, remember to turn off event forwarding from AppsFlyer, Adjust, or Branch. - **Properly set up attribution integration**: Ensure that attribution is set up in both your mobile app code and the Adapty Dashboard. Without both in place, Adapty won’t be able to send subscription events. - **Set a single attribution source**: Adapty can use attribution data in analytics from only one source at a time. If multiple attribution sources are enabled, the system will decide which attribution to use for each device based on the source that provides more fields. For iOS devices, this means non-organic [Apple Search Ads attribution](apple-search-ads) will always take priority if it's enabled. You can disable Apple Search Ads attribution collection by toggling off the **Receive Apple Search Ads attribution in Adapty** in the [**App Settings** -> **Apple Search Ads** tab](https://app.adapty.io/settings/apple-search-ads). - **Attribution data is never overwritten in analytics**: Attribution data is saved once after the user profile is created and won’t be overwritten in analytics once stored. ::: Follow our detailed guidance on configuring the following 3d-part attribution integrations: - [Adjust](adjust) - [Airbridge](airbridge) - [Apple Search Ads](apple-search-ads) - [AppsFlyer](appsflyer) - [Asapty](asapty) - [Branch](branch) - [Facebook Ads](facebook-ads) - [Singular](singular) - [Tenjin](tenjin) :::note Don't see your attribution provider? Let us know! [Write to the Adapty support](mailto:support@adapty.io) and we'll consider adding it. ::: ### Custom If you use another attribution system, you can pass the attribution data to Adapty. You can then segment users based on this data. To set attributes, use only the keys from the example below (all keys are optional). The system supports max 30 available attributes, where the keys are limited to 30 characters. Every value in the map should be no longer than 50 characters. `status` can only be `organic`, `non-organic` or `unknown`. Any additional keys will be omitted. ```swift showLineNumbers title="Swift" let attribution = [ "status": "non_organic|organic|unknown", "channel": "Google Ads", "campaign": "Christmas Sale", "ad_group": "ad group", "ad_set": "ad set", "creative": "creative id" ] Adapty.updateAttribution(attribution, source: "custom") ``` --- # File: audience.md --- --- title: "Audiences" description: "Learn how to segment and manage audiences in Adapty for targeted subscription offers." --- **Audiences** in Adapty are groups of users based on [segments](segments), helping you customize paywalls, onboardings, or A/B tests for specific user groups. You can define these segments using filters to ensure the right users see the right paywall or onboarding in your app. In Adapty, a **Placement** is where you can show paywalls, onboardings, or A/B tests. When you add an audience to a placement, you're targeting specific user groups with personalized content. For instance, you might show different paywalls based on a user's age, device, or subscription status. If a user falls into multiple groups, you can choose which group gets the priority, deciding which paywall they'll see. In the example below, we have an onboarding flow to display your placement with the `Onboarding` identifier. In your app code, you will access the placement using this identifier. If the user belongs to the "Yoga beginners" audience, they will see the first paywall. Those who do not fit the "Yoga beginners" audience will see the second paywall. To display a paywall, onboarding, or A/B test to a specific audience, do the following: 1. [Create a user segment](segments#creation). You can skip this step if you want to show the paywall or A/B test to all users. In such a case, use the "All users" audience created by default. 2. [Add this segment as an audience to placement and define which paywall or A/B test should be shown to it](add-audience-paywall-ab-test). The "All users" audience is automatically added to every placement; you only need to specify which paywall or A/B test should be displayed. 3. [Set the right priorities](change-audience-priority) if you have more than one audience in a placement. This ensures that users who belong to more than one audience will see the most relevant content. When a user is part of several audiences, the paywall for the highest-priority audience will be displayed. 4. [iOS](ios-quickstart-paywalls.md), [Android](android-quickstart-paywalls.md), [Flutter](flutter-quickstart-paywalls.md), [React Native](react-native-quickstart-paywalls.md), and [Unity](unity-quickstart-paywalls.md). --- # File: billing-issue.md --- --- title: "Billing issue" description: "Resolve subscription billing issues using Adapty’s support tools." --- The Billing issue chart displays the number of subscriptions that have entered the Billing Issue state. This state is typically triggered when the store, such as Apple or Google, is unable to receive payment from the subscriber for some reason. This could happen due to reasons such as an expired credit card or insufficient funds. ### Calculation Adapty calculates the billing issue chart by tracking the number of subscriptions that have entered the billing issue state during a given period. A subscription enters the Billing Issue state when the store (e.g. Apple, Google) is unable to process payment from the subscriber for some reason, such as an expired credit card or insufficient funds. During the Billing Issue state, the subscription is not considered active, and if the Grace Period feature is enabled in the store settings, the subscription will only move to the Billing Issue state after the Grace Period has expired. ### Available filters and grouping - ✅ Filter by: Attribution, country, paywall, store, product, and duration. - ✅ Group by: Product, country, store, paywall, duration, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative. You can find more information about the available controls, filters, grouping options, and how to use them in the[ this documentation.](controls-filters-grouping-compare-proceeds) ### Billing issue chart usage The Billing Issue chart can provide valuable insights into your app's subscription performance and revenue. By tracking the number of subscriptions in the Billing Issue state over time, you can identify patterns and potential issues related to payment processing and user payment information. This information can be used to optimize your app's payment process and reduce the likelihood of subscription cancellations due to payment issues. You can also use the chart to track the impact of changes to payment processing and billing information update flows. ### Similar metrics In addition to the Billing Issue chart, Adapty also provides metrics for other issues-related events, such as Refund events, Refund money, and Grace period. To learn more about these issue-related metrics, please refer to the following documentation: - [Refund money](new-trials) - [Refund events](active-trials) - [Grace period](trials-renewal-cancelled) --- # File: branch.md --- --- title: "Branch" description: "Integrate Branch with Adapty to track deep links and app conversions." --- [Branch](https://www.branch.io/) enables customers to reach, interact, and assess results across diverse devices, channels, and platforms. It's a user-friendly platform designed to enhance mobile revenue through specialized links that work seamlessly on all devices, channels, and platforms. Adapty provides a complete set of data that lets you track [subscription events](events) from stores in one place. With Adapty, you can easily see how your subscribers are behaving, learn what they like, and use that information to communicate with them in a way that's targeted and effective. The integration between Adapty and Branch operates in two main ways. 1. **Receiving attribution data from Branch** Once you've set up the Branch integration, Adapty will start receiving attribution data from Branch. You can easily access and view this data on the user's profile page. 2. **Sending subscription events to Branch** Adapty can send all subscription events which are configured in your integration to Branch. As a result, you'll be able to track these events within the Branch dashboard. ## How to set up Branch integration To integrate Branch go to [Integrations > Branch](https://app.adapty.io/integrations/branch) in Adapty Dashboard , turn on a toggle from off to on, and fill out fields. To get the value for the **Branch Key**, open your Branch [Account Settings](https://dashboard.branch.io/account-settings/profile) and find the **Branch Key** field. Use it for the **Key test** (for Sandbox) or **Key live** (for Production) field in the Adapty Dashboard. In Branch, switch between Live and Tests environments for the appropriate key. ## Events and tags Below the credentials, there are three groups of events you can send to Branch from Adapty. Simply turn on the ones you need. Check the full list of the events offered by Adapty [here](events). You can send an event with Proceeds \(after Apple/Google cut\) or just revenue. Also, you can check a box for reporting in the user's currency. We recommend using the default event names provided by Adapty. But you can change the event names based on your needs. Adapty will send subscription events to Branch using a server-to-server integration, allowing you to view all subscription events in your Branch dashboard and link them to your acquisition campaigns. ## SDK configuration It's very important to send Branch attribution data from the device to Adapty using `.setIntegrationIdentifier()` SDK method. The example below shows how to do that. To connect the Branch and Adapty user, make sure you provide your `customerUserId` to Branch. If you prefer not to use `customerUserId` in Branch, use the `setIntegrationIdentifier method to specify the Branch user ID. ```swift showLineNumbers do { try await Adapty.setIntegrationIdentifier( key: "branch_id", value: ) } catch { // handle the error } ``` ```kotlin showLineNumbers // login and update attribution and identifier Branch.getAutoInstance(this) .setIdentity("YOUR_USER_ID") { referringParams, error -> referringParams?.let { data -> Adapty.updateAttribution(data, "branch") { error -> if (error != null) { //handle the error } } } } // logout Branch.getAutoInstance(context).logout() ``` ```javascript showLineNumbers FlutterBranchSdk.setIdentity('YOUR_USER_ID'); ``` ```csharp showLineNumbers Branch.setIdentity("your user id"); ``` ```typescript showLineNumbers branch.setIdentity('YOUR_USER_ID'); ``` Next, pass the attribution you receive from the initializing method of Branch iOS SDK to Adapty. ```swift 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 { Adapty.updateAttribution(data, source: .branch) } } } } ``` ```kotlin showLineNumbers //everything is in the above snippet for Android ``` ```javascript showLineNumbers try { await Adapty().setIntegrationIdentifier( key: "branch_id", value: , ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` ```csharp showLineNumbers using AdaptySDK; Branch.initSession(delegate(Dictionary parameters, string error) { string attributionString = JsonUtility.ToJson(parameters); Adapty.UpdateAttribution( attributionString, "branch", (error) => { // handle the error }); }); ``` ```typescript showLineNumbers branch.subscribe({ enComplete: ({ params, }) => { adapty.updateAttribution(params, "branch"); }, }); ``` --- # File: braze.md --- --- title: "Braze" description: "Integrate Braze with Adapty for seamless customer engagement and push notifications." --- As one of the top customer engagement solutions, [Braze](https://braze.com/) provides a wide range of tools for push notifications, email, SMS, and in-app messaging. By integrating Adapty with Braze, you can easily access all of your subscription events in one place, giving you the ability to trigger automated communication based on those events. Adapty provides a complete set of data that lets you track [subscription events](events) from all stores in one place and can be used to update your users' profiles in Braze. With Adapty, you can easily see how your subscribers are behaving, learn what they like, and use that information to communicate with them in a way that's targeted and effective. Therefore, this integration allows you to track subscription events in your Braze dashboard and map them with your [acquisition campaigns.](https://www.braze.com/product/braze-canvas-flow) Adapty sends subscription events, user properties and purchases over to Braze, so you can build target communication with customers using Braze push notifications after a short and easy integration as described below. ## How to set up Braze integration To integrate Braze go to [Integrations -> Braze](https://app.adapty.io/integrations/braze), switch on the toggle, and fill out the fields. The initial step of the integration process is to provide the necessary credentials to establish a connection between your Braze and Adapty profiles. You will need the **REST API Key**, your **Braze Instance ID**, and **App IDs** for iOS and Android for the integration to work properly: 1. **REST API Key** can be created in **Braze Dashboard** → **Settings** → **API Keys**. Make sure your key has a `users.track` permission when creating it: 2. To get **Braze Instance ID** note your Braze Dashboard URL and go to the section of [Braze Docs](https://www.braze.com/docs/api/basics/#endpoints) where the instance ID is specified. It should have a regional form such as US-03, EU-01, etc. 3. iOS and Android App IDs can be found in Braze Dashboard → Settings → API Keys as well. Copy them from here: ## Events, user attributes and purchases Below the credentials, there are three groups of events you can send to Braze from Adapty. Simply turn on the ones you need. You may also change the names of the events as you need to send it to Braze. Check the full list of the Events offered by Adapty [here](events): Adapty will send subscription events and user attributes to Braze using a server-to-server integration, allowing you to view it in your Braze Dashboard and configure campaigns based on that. For events that have revenue, such as trial conversions and renewals, Adapty will send this info to Braze as purchases. [Here](messaging#event-properties) you can find the complete specifications for the event properties sent to Braze. :::note Helpful user attributes Adapty sends some user attributes for Braze integration by default. You can refer to the list of them provided below to determine which is best suited for your needs. ::: | User attribute | Type | Value | |--------------|----|-----| | `adapty_customer_user_id` | String | Contains the value of the unique identifier of the user defined by the customer. Can be found both in the Adapty [Dashboard](profiles-crm) and in Braze. | | `adapty_profile_id` | String | Contains the value of the unique identifier Adapty User Profile ID of the user, which can be found in the Adapty [Dashboard](profiles-crm). | | `environment` | String |

Indicates whether the user is operating in a sandbox or production environment.

Values are either `Sandbox` or `Production`

| | `store` | String |

Contains the name of the Store that used to make the purchase.

Possible values:

`app_store` or `play_store`.

| | `vendor_product_id` | String |

Contains the value of Product Id in Apple/Google store.

e.g., org.locals.12345

| | `subscription_expires_at` | String |

Contains the expiration date of the latest subscription.

Value format is:

YYYY-MM-DDTHH:mm:ss.SSS+TZ

e.g., 2023-02-15T17:22:03.000+0000

| | `active_subscription` | String | The value will be set to `true` on any purchase/renewal event, or `false` if the subscription is expired. | | `period_type` | String |

Indicates the latest period type for the purchase or renewal.

Possible values are

`trial` for a trial period or `normal` for the rest.

| All float values will be rounded to int. Strings stay the same. In addition to the pre-defined list of tags available, it is possible to send [custom attributes](segments#custom-attributes) using tags. This allows for more flexibility in the type of data that can be included with the tag and can be useful for tracking specific information related to a product or service. All custom user attributes are sent automatically to Braze if the user marks the ** Send user attributes** checkbox from [the integration page](https://app.adapty.io/integrations/braze). ## SDK Configuration To link user profiles in Adapty and Braze you need to either configure Braze SDK with the same customer user ID as Adapty or use its `.changeUser()` method: ```swift showLineNumbers let braze = Braze(configuration: configuration) braze.changeUser(userId: "adapty_customer_user_id") ``` ```kotlin showLineNumbers Braze.getInstance(context).changeUser("adapty_customer_user_id") ``` --- # File: cancelled-subscriptions.md --- --- title: "Subscriptions renewal cancelled" description: "Handle cancelled subscriptions efficiently with Adapty’s management tools." --- The Subscriptions renewal canceled chart displays the number of subscriptions that have had their auto-renew status switched off (canceled by the user). When a subscription's auto-renew status is turned off, it means that the subscription will not renew automatically for the next period. However, the user still retains access to the app's premium features until the end of the current period. ### Calculation Adapty's calculation logic for the subscriptions renewal cancelled chart involves counting the number of subscriptions that have had their auto-renewal status switched off during a given period. This includes subscriptions that were canceled by the user and will not renew automatically for the next period. ### Available filters and grouping - ✅ Filter by: Attribution, country, paywall, store, product, and duration. - ✅ Group by: Product, country, store, paywall, duration, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative. You can find more information about the available controls, filters, grouping options, and how to use them in [this documentation.](controls-filters-grouping-compare-proceeds) ### Subscriptions renewal canceled chart usage The Subscription renewal canceled chart is useful to get valuable insights into the number of recurring, individual paid users from your app. This metric serves as a proxy for the size and growth potential of a business. Combining active subscriptions with filters and grouping helps you to gain a deeper understanding of their paid subscriber base composition, making it a powerful tool for data analysis. ### Similar metrics In addition to Subscription renewal canceled chart, Adapty also provides metrics for other subscription-related events, such as active subscriptions, new subscriptions, expired subscriptions, and non-subscriptions. To learn more about these subscriptions-related metrics, please refer to the following documentation guides: - [Active subscriptions](active-subscriptions) - [Churned (expired) subscriptions](churned-expired-subscriptions) - [New subscriptions](reactivated-subscriptions) - [Non-subscriptions](non-subscriptions) --- # File: cantMakePayments-flutter.md --- --- title: "Fix for Code-1003 cantMakePayment error in Flutter SDK" description: "Resolve making payments error when managing subscriptions in Adapty." --- --- # File: cantMakePayments-react-native.md --- --- title: "Fix for Code-1003 cantMakePayment error in React Native SDK" description: "Resolve making payments error when managing subscriptions in Adapty." --- --- # File: cantMakePayments-unity.md --- --- title: "Fix for Code-1003 cantMakePayment error in Unity SDK" description: "Resolve making payments error when managing subscriptions in Adapty." --- --- # File: cantMakePayments.md --- --- title: "Fix for Code-1003 cantMakePayment error" description: "Resolve making payments error when managing subscriptions in Adapty." --- --- # File: change-audience-priority.md --- --- title: "Change audience priority in placement" description: "Adjust audience priorities in Adapty to target users with personalized offers." --- When you have different user audiences in one [placement](placements), a user can belong to more than one audience. For instance, if you've defined audiences like "Beginners", "Runners", and a general audience like "All users", it's crucial to determine which specific audience to consider first when a user falls into multiple categories. In this scenario, we rely on audience priority. Audience priority is a numerical order, where #1 is the highest. It guides the sequence for audiences to check. In simpler terms, audience priority helps Adapty make decisions about which audience to apply first when selecting the paywall, onboarding, or A/B test to display. If the audience priority is low, users who potentially qualify might be bypassed. Instead, they could be directed to another audience with a higher priority. Crossplacement audiences, meaning those created for [crossplacement A/B tests](ab-tests#ab-test-types), always take priority over regular audiences. The "All users" audience always has the lowest priority since it’s a fallback and includes everyone who doesn’t match any other audience. To adjust audience priorities for a placement: 1. While creating a new or editing an existing placement, click **Edit priority**. The button is visible only if at least three audiences are added to a placement ("All users" and two others). If less, the order is obvious - the "All users" audience comes last. 2. In the opened **Edit audience priorities** window, drag-and-drop audiences to reorder them correctly. 3. Click the **Save** button. --- # File: charts.md --- --- title: "Analytics charts" description: "Use Adapty’s charts and analytics to track and improve subscription performance." --- Adapty's charts provide a comprehensive view of your app's performance metrics, enabling you to quickly and easily track key data points like user engagement, retention, and revenue. To access any of Adapty's charts, navigate to the [Charts](https://app.adapty.io/analytics/charts/) section in the Analytics dashboard. Adapty charts are built using real-time data, so you can always stay up-to-date with the latest metrics. We also offer advanced controls that allow you to filter and group data by a range of criteria, including country, paywall, and product, giving you even greater flexibility and insight. Adapty offers a wide range of charts to help you analyze and visualize your app's performance data. Here are the charts currently available in Adapty: ### Subscription and in-app charts - [Revenue](revenue): Track your app's total revenue, broken down by date or other criteria. - [MRR](mrr) (Monthly recurring revenue): See how your app's recurring revenue is performing month-over-month. - [ARR ](arr)(Annual recurring revenue): Track your app's recurring revenue on an annual basis. - [ARPU ](arpu)(Average revenue per user): Measure how much revenue your app is generating per user. - [ARPPU](arppu) (Average revenue per paying user): Measure how much revenue your app is generating per paying user. - [Installs](installs): Monitor the number of app installs over a specific period. - [Active subscriptions](active-subscriptions): Keep track of the number of active subscriptions in your app. - [New subscriptions](reactivated-subscriptions): Analyze the number of new subscriptions generated in a specific period. - [Subscription renewal cancelled](cancelled-subscriptions): Track the number of subscriptions that were canceled before renewal. - [Expired (churned) subscriptions](churned-expired-subscriptions): See the number of expired subscriptions or users who have churned. - [Non-subscriptions](non-subscriptions): Analyze the revenue generated by non-subscription in-app purchases. ### Trials charts - [Active trials](active-trials): Monitor the number of active free trials in your app. - [New trials](new-trials): Analyze the number of new trials generated in a specific period. - [Trial renewal cancelled](trials-renewal-cancelled): Track the number of trials that were canceled before renewal. - [Expired (churned) trials](expired-churned-trials): See the number of expired trials. ### Issues charts - [Grace period](grace-period): Analyze how long users are in the grace period after a failed payment. - [Billing issues](billing-issue): Track the number of failed payments, chargebacks, and other billing issues. - [Refund events](refund-events): Monitor the number of refunds processed by your app. - [Refund money](refund-money): Analyze the amount of revenue lost due to refunds. Each of these charts provides a unique perspective on your app's performance, helping you identify trends, track progress, and make data-driven decisions. Adapty's charts are designed to be highly customizable, giving you complete control over how you view and analyze your app's performance data. To get the most out of our charts, check our [Analytics controls](controls-filters-grouping-compare-proceeds) documentation, which provides a detailed overview of all the features and functionality available. --- # File: china-cluster.md --- --- title: "Adapty on China servers" description: "Understand Adapty's China cluster and its data storage policies." no_index: true --- To ensure that your application is not blocked in China and works efficiently, Adapty provides a China cluster option. This ensures fast and reliable service for your users in mainland China while helping you comply with local regulations. The Great Firewall of China can significantly impact connectivity and performance for applications using servers hosted outside the country. However, Adapty's China-based infrastructure allows your application to deliver consistent, reliable performance to users in mainland China. :::important In China, applications require explicit user permission to access the internet. Until this permission is granted, **no network requests will work**. Ensure your application: - Requests internet access permission appropriately. - Handles cases where permission is denied or not yet granted. - Provides clear user guidance about why internet access is needed. ::: ## Step 1. Configure Adapty SDK for China region [iOS](sdk-installation-ios), [Android](sdk-installation-android), [Flutter](sdk-installation-flutter), [React Native](sdk-installation-reactnative), and [Unity](sdk-installation-unity). For the China region, when configuring the SDK during the installation, follow the instructions depending on your framework: During configuration, add the China cluster as follows: For Adapty iOS SDK 3.6.0 or later, set the cluster as `.with(serverCluster: .cn)`. For older versions, include the `backendBaseUrl` parameter in your configuration: `.with(backendBaseUrl: URL(string: "https://api-cn.adapty.io/api/v1")!)`. ```swift showLineNumbers // In your AppDelegate class: let configurationBuilder = AdaptyConfiguration .Builder(withAPIKey: "PUBLIC_SDK_KEY") .with(observerMode: false) .with(customerUserId: "YOUR_USER_ID") // highlight-next-line .with(serverCluster: .cn) 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 // highlight-next-line .with(serverCluster: .cn) Adapty.activate(with: configurationBuilder.build()) { error in // handle the error } } var body: some Scene { WindowGroup { ContentView() } } } ``` Parameters: | Parameter | Description | | ------------------ | ------------------------------------------------------------ | | **backendBaseUrl** | Use the `URL(string: "https://api-cn.adapty.io/api/v1")!` value to connect your app to Adapty's China servers. | During configuration, add the China cluster as follows: Use the `.withServerCluster` method in your configuration: ```kotlin showLineNumbers override fun onCreate() { super.onCreate() Adapty.activate( applicationContext, AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withObserverMode(false) //default false .withCustomerUserId(customerUserId) //default null // highlight-next-line .withServerCluster(AdaptyConfig.ServerCluster.CN) .build() ) } ``` ```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 // highlight-next-line .withServerCluster(AdaptyConfig.ServerCluster.CN) .build() ); } ``` Added method: | Method | Description | | --------------------- | ------------------------------------------------------------ | | **withServerCluster** | Pass the value `AdaptyConfig.ServerCluster.CN` to it to connect your app to Adapty's China servers. | For React Native applications, configure the China cluster as follows: ```javascript showLineNumbers // Initialize Adapty with China servers await Adapty.activate({ sdkKey: 'PUBLIC_SDK_KEY', observerMode: false, // optional, default false customerUserId: 'YOUR_USER_ID', // optional // highlight-next-line serverCluster: 'cn', // Use 'cn' for China servers }); ``` Parameters: | Parameter | Description | | ----------------- | ------------------------------------------------- | | **serverCluster** | Use the value `'cn'` to connect to China servers. | For Flutter applications, configure the China cluster as follows: ```dart showLineNumbers await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'PUBLIC_SDK_KEY') ..withServerCluster(AdaptyServerCluster.cn) ..withCustomerUserId('YOUR_USER_ID'), ); ``` Parameters: | Parameter | Description | | ----------------- | ----------------------------------------------------------- | | **serverCluster** | Use the value `AdaptyServerCluster.cn` for China servers. | After configuring the China server cluster, you can use the Adapty Dashboard as usual at `app.adapty.io`. The dashboard experience is identical regardless of which server cluster you're using. ## Step 2. Detect when to use China servers It's important to dynamically choose between global and China-specific servers based on user location. Here are two approaches: ### Option 1: Detect by region/country ```swift showLineNumbers func shouldUseChinaServers() -> Bool { Locale.current.regionCode == "CN" } // In your configuration let baseURL = shouldUseChinaServers() ? URL(string: "https://api-cn.adapty.io/api/v1")! : URL(string: "https://api.adapty.io/api/v1")! // Default URL let configurationBuilder = AdaptyConfiguration .Builder(withAPIKey: "PUBLIC_SDK_KEY") .with(serverCluster: .cn) // other configuration options ``` ```javascript showLineNumbers async function initializeAdapty() { // Determine if user is in China const shouldUseChinaServers = () => getCountry() === "CN"; // Initialize with appropriate server await Adapty.activate({ sdkKey: 'PUBLIC_SDK_KEY', serverCluster: shouldUseChinaServers ? 'cn' : 'default', // other configuration options }); } // Call the initialization function initializeAdapty(); ``` ```kotlin // Define the helper function private fun shouldUseChinaServers() = Locale.getDefault().country == "CN" // Use it in configuration private fun setupAdapty() { val serverCluster = if (shouldUseChinaServers()) AdaptyConfig.ServerCluster.CN else AdaptyConfig.ServerCluster.DEFAULT Adapty.activate( context, AdaptyConfig.Builder("PUBLIC_SDK_KEY") // other configuration options .withServerCluster(serverCluster) .build() ) } ``` ### Option 2: Detect by app store For applications distributed through different app stores, you can determine which server to use based on the installation source: ```swift showLineNumbers func shouldUseChinaServers() async -> Bool { let code: String? if #available(iOS 15.0, *) { code = await Storefront.current?.countryCode } else { code = SKPaymentQueue.default().storefront?.countryCode } return code == "CHN" } // In your configuration let configurationBuilder = AdaptyConfiguration .Builder(withAPIKey: "PUBLIC_SDK_KEY") // Only set server cluster if it's China if await shouldUseChinaServers() { configurationBuilder.with(serverCluster: .cn) } // other configuration options ``` ```javascript showLineNumbers async function initializeAdapty() { // For Android, determine if the app was installed from a Chinese app store // For iOS, you might check the storefront country const shouldUseChinaServers = Platform.OS === 'android' ? await isChineseStore() : await isChineseStorefront(); // Initialize with appropriate server await Adapty.activate({ sdkKey: 'PUBLIC_SDK_KEY', serverCluster: shouldUseChinaServers ? 'cn' : 'default', // other configuration options }); } // Example implementation for Android (simplified) async function isChineseStore() { try { // Check if the app was installed from a Chinese app store // This is a simplified example - implement proper store detection based on your distribution channels const { installerPackageName } = await NativeModules.InstallerInfo.getInstallerInfo(); const chineseStores = [ 'com.huawei.appmarket', // Huawei AppGallery 'com.xiaomi.market', // Xiaomi GetApps 'com.oppo.market', // OPPO App Market 'com.vivo.appstore', // Vivo App Store // Add other Chinese app stores as needed ]; return chineseStores.includes(installerPackageName); } catch (error) { console.error('Error detecting store:', error); return false; } } // Example implementation for iOS (simplified) async function isChineseStorefront() { // Implementation depends on how you detect the App Store region // This would typically involve checking receipt information or other store data // Simplified example: return await NativeModules.StoreHelper.isChineseStorefront(); } // Call the initialization function initializeAdapty(); ``` --- # File: choose-meaningful-placements.md --- --- title: "Choose meaningful placements" description: "Optimize paywall placements with Adapty for increased user engagement and revenue." --- When [creating placements](create-placement), it's essential to consider the logical flow of your app and the user experience you want to create. Most apps should have no more than 5 [placements](placements) without sacrificing the ability to run experiments. Here's an example of how you can structure your placements: 1. **Onboarding flow:** This stage represents the first interaction your users have with your app. It's an excellent opportunity to introduce your users to your app's value proposition by using both onboarding and paywall placements here. Over 80% of subscriptions are activated during onboarding flow, so it's important to focus on selling the most profitable subscriptions here. With Adapty, you can easily have different [onboardings](https://adapty.io/docs/onboardings) and [paywalls](https://adapty.io/docs/paywalls) for different audiences, and run A/B tests to find the best option for your app. For example, you can run an A/B test for users from the US, showing more expensive subscriptions 50% of the time. 2. **App settings:** If the user hasn't subscribed during the onboarding flow, you can create a paywall placement within your app. This can be in the app settings or after the user has completed a specific target action. Since users inside the app tend to think more thoroughly about subscribing, the products on this paywall might be slightly less expensive compared to those in the onboarding stage. 3. **Promo:** If the user hasn't subscribed after seeing the paywall multiple times, it could indicate that the prices are too high for them or they are hesitant about subscriptions. In this case, you can show a special offer to them with the most affordable subscription or even a lifetime product. This can help entice users who are price-sensitive or skeptical about subscriptions to make a purchase. Most apps will have similar logic and placements, following the user journey and key points where paywalls, onboardings, or A/B tests can be displayed to drive conversions and revenue. You can configure them in each placement to experiment and optimize your monetization strategies. --- # File: churned-expired-subscriptions.md --- --- title: "Churned (expired) subscriptions" description: "Manage churned and expired subscriptions to improve user retention." --- The churned (expired) subscriptions chart displays the number of subscriptions that have expired, meaning that the user no longer has access to the premium features of the app. Typically, this occurs when the user decides to stop paying at the end of the subscription period for the app or encounters a billing issue. ### Calculation The churned (expired) subscriptions chart calculation logic for Adapty involves counting the number of subscriptions that have expired during a given period. This includes users who have decided to stop paying for the app or those who have experienced billing issues. To obtain this chart, the number of expired subscriptions should be counted daily or monthly. At a daily resolution, the count of expired subscriptions represents the number of subscriptions that expired on that day, while at a monthly resolution, it represents the number of expired subscriptions during that month. ### Available filters and grouping - ✅ Filter by: Attribution, country, paywall, store, product, and duration. - ✅ Group by: Expiration reason, product, country, store, paywall, duration, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative. You can find more information about the available controls, filters, grouping options, and how to use them in [this documentation.](controls-filters-grouping-compare-proceeds) ### Churned subscriptions chart usage The Churned (expired) subscriptions chart is a useful metric to gain insights into the number of users who have stopped paying for the app or have experienced billing issues during a specific period. This metric provides information on the number of users who have churned, which can be used to identify trends in user behavior and billing issues. By combining the Churned subscription chart with filters and grouping, app developers or business owners can gain a deeper understanding of their user base and analyze the reasons for churn. ### Similar metrics In addition to Churned subscriptions, Adapty also provides metrics for other subscription-related events, such as active subscriptions, new subscriptions, subscriptions renewal canceled, and non-subscriptions. To learn more about these subscriptions-related metrics, please refer to the following documentation guides: - [Active subscriptions](active-subscriptions) - [New subscriptions](reactivated-subscriptions) - [Cancelled subscriptions](cancelled-subscriptions) - [Non-subscriptions](non-subscriptions) --- # File: configuration.md --- --- title: "Configure 3d-party integration" description: "Learn how to configure Adapty settings to optimize subscription management." --- With Adapty integrations, you can seamlessly transmit subscription events and purchase data to your preferred platform or workflow. Whether you're seeking user behavior insights, customer engagement strategies, or enhanced product analytics for your marketing team, Adapty can effortlessly forward in-app purchase events to your chosen integration. Adapty effortlessly tracks in-app purchases and subscription events such as trials, conversions, renewals, and cancellations. These [events](events) are automatically communicated to your chosen integrations. This allows you to engage with customers based on their current stage and analyze revenue-related activities within your app. ## Integration settings Integrations offer the following configuration options that impact all events sent through this integration: | Setting | Description | | :--------------------------------- | :----------------------------------------------------------- | | **Reporting Proceeds** | Select how revenue values are presented: either net of App Store and Play Store commissions or gross (before deductions). Toggle the "Send sales as proceeds" checkbox to display sales as proceeds after App Store / Play Store commissions have been subtracted. | | **Send Trial Price** | If checked, Adapty will transmit the subscription price for the Trial Started event. | | **Exclude Historical Events** | Opt to exclude events that occurred before the user installed the app with Adapty SDK. This prevents duplication of events and ensures accurate reporting. For instance, if a user activated a monthly subscription on January 10th and updated the app with Adapty SDK on March 6th, Adapty will omit events before March 6th and retain subsequent events. | | **Report User's Currency** | Choose whether sales are reported in the user's currency or in USD. | | **Send User Attributes** | If you wish to send user-specific attributes, like language preferences, and your OneSignal plan supports more than 10 tags, select this option. Enabling this allows the inclusion of additional information beyond the default 10 tags. Note that exceeding tag limits may result in errors. | | **Send Attributions** | Enable this option to transmit attribution information (e.g. AppsFlyer attribution) and receive relevant details. | | **Send Play Store purchase token** | Enable this option to receive the Play Store token needed to revalidate the purchase if required. It will add the `play_store_purchase_token` parameter to the event. | ## Configure the events Below the credentials, there are three groups of events you can send to the selected integration platform from Adapty. You should turn on the ones you need. It's important to note that event name customization is available for certain integrations, while for others, the event names are set and cannot be modified. Additionally, with certain integrations such as [Airbridge](airbridge#events-and-tags) for example, you have the flexibility to associate multiple event names with a single Adapty event. Check the full list of the Events offered by Adapty [here](events). While we recommend utilizing Adapty's default event names, you have the freedom to adapt event names as per your specific requirements. --- # File: controls-filters-grouping-compare-proceeds.md --- --- title: "Analytics controls" description: "Control and filter revenue data with Adapty’s powerful analytics tools." --- Adapty offers a wide range of controls to help you gain valuable insights and unlock the full potential of your data and gain a comprehensive view of your business performance. Whether you're analyzing charts, cohorts, funnels, retention, conversion data, or LTV, these controls provide powerful functionality. By leveraging these controls, you can dive deeper into your data and extract meaningful insights to drive your business decisions. In the following article, you can learn more about each control and how to use them effectively. Additionally, you'll find information about which controls are supported for each type of analytics, including charts, cohorts, funnels, retention, and conversion. | Control | Charts | Cohorts | Funnels | Retention | Conversion | LTV | | :-------------- | :----- | :------ | :------ | :-------- | :--------- | :-- | | Time ranges | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Data comparison | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | | Filtering | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Grouping | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Chart views | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | | Table view | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | CSV data export | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | Proceeds | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | | Taxes | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ### Time ranges When using the Adapty calendar to set a time range for a chart, you have several convenient options to choose from. These options determine the date range displayed on the x-axis of the charts, allowing you to focus on specific periods of time. Here are the available quick options for time ranges: - **Last 7 days**: Displays data for the most recent 7-day period. - **Last month**: Shows data from the current date to the same day in the previous month. - **Last 28 days**: Useful for tracking weekly subscription products, as it covers the last four weeks. - **Last 3 months**: Displays data from the current date to three months ago. - **Last 6 months**: Shows data from the current date to six months ago. - **Last year**: Displays data from the current date to one year ago. - **Previous month**: Covers the full calendar month before the current month. - **This month**: Shows data from the 1st day of the current month until today. - **This quarter**: Displays data from the 1st day of the current quarter until today. - **This year**: Covers data from the 1st day of the current year until today. In addition to these predefined options, you can also select the **Custom** option to set a specific time period of your choice. This flexibility allows you to analyze your data in more granular detail or focus on specific events or campaigns. Apart from selecting specific time ranges, you can also adjust the time scale of charts. By choosing a day timescale, you can view the most detailed level of data, while opting for lower resolutions such as week, month, quarter, or year, allows you to identify longer-term trends. For charts, the time scale determines the scale of the grid on the x-axis and the resolution at which the data is displayed. The available time scale options include day, week, month, quarter, or year. Choosing a specific time scale allows you to view the data at different resolutions, helping you identify shorter-term or longer-term trends. When analyzing cohorts in Adapty, you choose the cohort length, which determines the size of each cohort and the grouping of users based on a specific time period. The time frame in cohorts analysis refers to the duration for which you want to analyze user behavior, and it helps define the boundaries of the cohorts. In LTV (lifetime value) analysis, the cohort length is chosen in the grouping settings and determines the time period over which you want to calculate the lifetime value of users. Similar to cohorts analysis, the cohort length in LTV analysis is independent of the time frame displayed on the x-axis of the charts. We have 2 formats of date and time - American and European. You can set one of them in your Adapty account as described [here](account). Please also note that all charts in Adapy analytics are displayed in UTC time. ### Data comparison To analyze the dynamics of your app's metrics, you can utilize the comparison feature located next to the calendar. It offers a convenient way to compare your metrics with the previous period, although you also have the flexibility to customize the comparison range based on your specific requirements. Here's how you can interpret the insights provided by the comparison feature: - **Comparison Display:** After selecting the comparison period, you can toggle between displaying the comparison on the chart or as a numerical value only. - **Difference Indicator:** The comparison shows the variance between your current result and the result from the previous period. Higher values are indicated in green, while lower values are indicated in red. - **Chart Visualization:** If you have no grouping or only one grouping selected, the comparison will be displayed on the chart as well. You can choose from different chart types such as area, line, or column to visually highlight the differences. - **Detailed Tooltip:** Hovering over the chart will reveal a tooltip with additional details, allowing you to examine the specifics of the comparison. - **Multiple Grouping and Comparison:** If you have multiple grouping options enabled, you can view multiple comparisons simultaneously on a single chart. This feature is available specifically for column charts. ### Filtering and grouping Filters play a crucial role in refining the data displayed in charts by including only the information that matches specific attributes. This feature becomes especially handy when you wish to examine the performance of a particular property, such as a specific country or product identifier. By grouping chart data, you can analyze the individual components that make up the chart totals. This is particularly valuable when you want to evaluate the performance of particular properties. It's important to note that certain charts may not support every type of filtering or grouping. To determine the compatibility of filters and grouping options with each chart, you can refer to the corresponding information provided on the respective chart description page. In Advanced Analytics, you have access to the following filtering and grouping options, empowering you to refine your data analysis: | | Filtering | Grouping | Description | |:---------------------| :-------- | :------- |:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Attribution | ✅ | ✅ | Filter or group metrics by Attribution fields like Status, Channel, Campaign, Adgroup, Adset, and Creative. | | Renewal Status | ❌ | ✅ | Group metrics by the product's renewal status, which indicates whether or not the subscription will be renewed in the next period. | | Period | ✅ | ✅ | Filter or group metrics by the subscription period. | | Country | ✅ | ✅ | Filter or group metrics by user's store country (if available, otherwise country is inferred using currency code or device's IP). | | Offer Type | ✅ | ❌ | Filter metrics by the type of applied offer. Available offer types include:
  • **Introductory**: A paid introductory offer giving a discount on the initial subscription period. Includes App Store Introductory Offers and Play Store paid offers (applied to initial subscription periods), but does **not** include free trial periods.
  • **Promotional**: A promotional offer applied to the subscription, such as App Store Promotional Offers.
  • **Offer Code**: A promo code the customer enters to receive a discount or free trial (depending on the offer and store). Includes promo codes from both the App Store and Play Store.
  • **No Offer**: Indicates no offer was applied to the transaction.
| | Offer ID | ✅ | ❌ | Filter metrics by a specific offer ID. | | Offer Discount Type | ✅ | ❌ | Filter metrics by the type of offer discount applied. Options include **Free Trial**, **Pay As You Go**, and **Pay Up Front**. | | Paywall (Deprecated) | ✅ | ✅ | Filter or group metrics by paywalls that are associated with purchases. ( Deprecated legacy option) | | Paywall | ✅ | ✅ | Filter or group metrics by [paywalls ](paywalls)that are associated with purchases. | | A/B Test | ✅ | ❌ | Filter or group metrics by [A/B tests](ab-tests) that are associated with purchases. | | Placement | ✅ | ✅ | Filter or group metrics by [placement](placements) that are associated with purchases. | | Store | ✅ | ✅ | Filter or group metrics by store (iOS/Android). | | Product | ✅ | ✅ | Filter or group metrics by [product](product) (both subscriptions and one-time purchases are available). | | Duration | ✅ | ✅ | Filter or group metrics by product's duration. | | Segment | ✅ | ✅ | Filter by user [segments](segments) to view specific user group performance. Group by segment to compare segment performance or contribution within **All users**.
Note:
  • Grouping by segments is not supported for Funnels.
  • Changing a custom attribute after it's used in a segment may unsync the user from that segment in analytics. Data will reflect the previous value.
| | Refund reason | ✅ | ✅ | Filter or group metrics by refund reason. Useful for distinguishing upgrade-related refunds from actual user-initiated refunds. | ### Chart views The Analytics section provides you with the flexibility to view each chart in different visual representations, such as stacked column, stacked area, line, 100% stacked column, and 100% stacked area. By selecting the appropriate view, you can effectively communicate the information displayed on the chart and enhance your data analysis experience. To change the view of a chart, simply locate and select the view dropdown menu, and then choose the desired representation option ### Table view In addition to the chart view, Adapty also provides a table view for each chart. The table view presents the underlying data used to generate the chart in a tabular format, allowing users to view and analyze the data in a more granular way. The table view is a useful tool for users who prefer to work with data in a more structured format or need to export the data for further analysis outside of Adapty. ### CSV data export To analyze the raw data behind charts, cohort analyses, funnels, retentions, or conversion analytics, you can export it in CSV format by clicking the **Export** button. You can also [retrieve the same data via the API](export-analytics-api). Regardless of the method, the data file will be identical. This feature gives you access to the underlying data, which you can further analyze in spreadsheet applications or other tools to gain deeper insights. ### Store commission and taxes One crucial aspect of revenue calculation is the inclusion of taxes (which can vary based on the user's store account country) and store commission fees. Adapty currently supports commission and tax calculation for both App Store and Play Store. In the charts tab of the Analytics section, Adapty introduces a dropdown field with three display options. The dropdown allows you to choose how the revenue is displayed in the chart. The available options are as follows: #### Gross revenue This option displays the total revenue, including taxes and commission fees from both App Store / Play Store. It represents the complete revenue generated by transactions before any deductions. #### Proceeds after store commission This option displays the revenue amount after deducting the store commission fee. It represents the revenue that remains after the App Store / Play Store cuts its commission fees from the gross revenue. Taxes are not deducted in this display option. Apple and Google take up to 30% of the price paid by the customers as a fee. For the apps included in Small Business Program (i.e. the app makes less than $1m per year), the fee is always 15%. The rest of the apps (>$1m per year) pay 30% by default and 15% for subscriptions that are consecutively renewed for at least a year. For detailed information on how Adapty calculates store commission fees, please refer to the corresponding documentation for [App Store](app-store-small-business-program) and [Play Store.](google-reduced-service-fee) #### Proceeds after store commission and taxes This option displays the revenue amount after deducting both the store commission fee and taxes. It represents the net revenue received by the app after accounting for both store's commission and applicable taxes. We consider the VAT rate of the user's store account country when calculating taxes. Please consider that Adapty follows the logic that for Apple taxes are applied to the post-commission revenue from a transaction, while Google applies taxes to the full amount (before store commissions are reduced from the revenue). It's important to note that this revenue dropdown applies to various revenue-related charts, such as [Revenue](revenue), [MRR](mrr) (Monthly recurring revenue), [ARR](arr) (Annual recurring revenue), [ARPU](arpu) (Average revenue per user), and [ARPPU](arppu) (Average revenue per paying user). These charts provide valuable insights into revenue-related information for your app. --- # File: create-access-level.md --- --- title: "Create access level" description: "Create and assign access levels in Adapty for better user segmentation." --- Access levels let you control what your app's users can do in your mobile app without hardcoding specific product IDs. Each product defines how long the user gets a certain access level for. So, whenever a user makes a purchase, Adapty grants access to the app for a specific period (for subscriptions) or forever (for lifetime purchases). When you create an app in the Adapty Dashboard, the `premium` access level is automatically generated. This serves as the default access level and it cannot be deleted. To create a new access level: 1. Open the **[Paywalls and Products](https://app.adapty.io/access-levels) ** section from the Adapty main menu, then select the **Access levels** tab. 2. Click the **Create access level** button. 3. In the **Create access level** window, assign it an ID. This ID will serve as the identifier within your mobile app, enabling access to additional features upon user purchase. Additionally, this identifier aids in distinguishing one access level from others within the app. Ensure it's clear and easily understandable for your convenience. 4. Click the **Create access level** button to confirm the access level creation. --- # File: create-offer.md --- --- title: "Create offer" description: "Create and manage special subscription offers using Adapty’s tools." --- Adapty allows you to offer discounted pricing to existing or churned subscribers. To use this feature, you need to first [create the offer in App Store Connect](app-store-offers) and/or [create the offer in Play Console](google-play-offers). :::note Introductory offers on iOS are applied automatically if the user is eligible. Do not create them in Adapty. ::: Once your promotional offer (for both the Play Store and App Store) or Win-back offer (for the App Store) is set up in the app stores, adding it to Adapty is simple: 1. Open the [**Paywalls and Products**](https://app.adapty.io/products) section from the main menu in Adapty, then select the **Products** tab. 2. Find the product you want to add an offer to. In the **Actions** column, click the **3-dot** button next to the product and select **Edit**. 3. In the **Edit product** window that opens, click **Add Offer** in the **Offers** tab. 4. Then enter the offer details for the product. Here are the fields for the offer: - **Offer name**: Give the offer a name to help identify it in Adapty. Use any name that’s convenient for you. - **App Store Offer type**: Select the type of App Store offer you’re adding: Promotional or Win-back. (Introductory offers don’t need to be added—they apply automatically if available.) - **App Store Offer ID**: This is the unique ID for the offer [that you set in the App Store](app-store-products). - **Play Store Offer ID**: Similarly, this is the unique ID for the offer [that you set in the Play Store](android-products). 5. (optional) Add more offers if needed by clicking **Add offer**. 6. Click **Save** to add the offers to the product. --- # File: create-onboarding.md --- --- title: "Create onboarding" --- [Onboardings](onboardings.md) introduce new users to your mobile app's value, features, and usage tips. ## Step 1. Create an onboarding To create a new onboarding in the Adapty dashboard: 1. Go to **Onboardings** from the Adapty main menu. This page gives an overview of all onboardings you’ve set up, along with their metrics. Click **Create onboarding**. 2. Create a descriptive name for your onboarding and click **Proceed to build onboarding**. 3. You will be redirected to the onboarding builder. It contains a default demo template, which you can study to understand how onboardings collect data and how you can [personalize them using variables and quizzes](onboarding-user-engagement.md). Feel free to remove any screens you don't need and [design your own onboarding experience](design-onboarding.md) there. 4. When ready, click the **Preview** button at the top right. Complete your onboarding flow yourself to ensure everything works as expected. 5. If everything works fine, click **Publish** at the top right. Please wait until it is published before getting back to Adapty. Otherwise, your progress will be lost. :::danger If you don't click **Publish**, the SDK won't be able to get the onboarding you've created. ::: After your onboarding is published, click **Back to Adapty**. Your onboarding is created, and you can add it to a placement to start using it. ## Step 2. Create a placement for your onboarding 1. Go to **Placements** from the main menu and switch to the **Onboardings** tab. Click **Create placement**. 2. Enter the placement name and ID. Then, click **Run onboarding** and select an onboarding to show to all users. 3. If you have a separate onboarding prepared for a specific user group, [add more audiences](#audience) and select a different onboarding for them. ## Step 3. Integrate the onboarding into your app :::important Onboardings are available only for apps using Adapty iOS, Android, or Flutter SDK version 3.8.0 or higher. ::: To start displaying onboardings in your app, integrate them using Adapty SDK: - [iOS](ios-onboardings.md) - [Android](android-onboardings.md) - [Flutter](flutter-onboardings.md) - [React Native](react-native-onboardings.md) To understand which onboarding works better, you can also run [A/B tests](ab-tests.md). --- # File: create-paywall.md --- --- title: "Create paywall" description: "Learn how to create high-converting paywalls using Adapty’s Paywall Builder." --- A [paywall](paywalls) serves as an in-app storefront where customers can browse products and make purchases.
Before you start creating paywalls (Click to Expand) 1. [Create at least one product](create-product). 2. (optional) [Create offer](create-offer).
To create a new paywall in the Adapty dashboard: 1. Go to [**Paywalls**](https://app.adapty.io/paywalls) in the Adapty main menu. This page shows an overview of all your paywalls and their metrics. 2. Click **Create paywall**. 3. On the **Paywalls / New paywall** page, enter a **Paywall name** to identify this paywall throughout the Adapty Dashboard. 4. Click **Add product**. 5. Select products to be shown to your customers. :::note - The product order in this list will be maintained in the SDK, so arrange products in your desired order. - Once a paywall is shown in production, you won’t be able to change the products on it, as this could affect paywall metrics. ::: 6. If you're offering free trials or other offers for your products, add them here or they won't be available. Choose an offer you [created earlier](create-offer) for this product from the **Offer** list. The list is only available for products that have offers. 7. Click **Create as a draft** to confirm paywall creation. Your paywall is now created! You can [add it to a placement](add-audience-paywall-ab-test) to start using it. --- # File: create-placement.md --- --- title: "Create placement" description: "Create and manage placements in Adapty to improve paywall performance." --- A [Placement](placements) is a specific location in your mobile app where you can show a paywall, onboarding, or A/B test. For example, a subscription choice might appear in a startup flow, while a consumable product (like golden coins) could show when a user runs out of coins in a game. You can show the same or different paywalls, onboardings, or A/B tests in various placements or to different user segments — called "audiences" in Adapty. You can use the same paywall or A/B test across multiple placements. You can also show different paywalls or A/B tests in one placement to different user segments (called audiences in Adapty). Read the [Choose meaningful placements](choose-meaningful-placements) section for tips on picking the right placement. :::info Although the placement creation process is similar for paywalls and onboardings, you can't create one placement for both as they process different metrics. ::: To create a new placement: 1. Go to **[Placements](https://app.adapty.io/placements)** from the Adapty main menu. 2. Click **Create placement**. 3. Enter a **Placement name**. This is an internal identifier in the Adapty Dashboard. You can edit it later if needed. 4. Enter a **Placement ID**. You'll use this ID in the Adapty SDK to call the placement's [paywalls](paywalls) and [A/B tests](ab-tests). You cannot edit it later as it's unique for each placement. 5. Click **Run Paywall** or **Run A/B test** depending on the placement's purpose. Learn more in the section below. ## Adding paywalls, onboardings, and A/B tests Adapty lets you show paywalls, onboardings, and A/B tests to specific [audiences](audience) — user segments created to better target your content. If you don't need targeting, use the default *All users* audience. If you use audiences, start by adding a paywall, onboarding, or A/B test to the *All users* audience. Then add more audiences to your placement. Learn more on the [Audiences](audience) page. ## Adding paywalls and A/B tests Adapty supports showing paywalls and A/B tests to specific [audiences](audience). Audiences are groups of users based on [segments](segments.md). - If you don't need audiences, add your paywall or A/B test to the default *All users* audience and you're done. - If you use audiences, first add a paywall or A/B test to *All users*. Then add more audiences to the placement. Find more details in the [Audiences](audience) page. :::note To proceed, ensure that you created a paywall, onboarding, or A/B test you want to run and an audience, you'd like to specify. ::: 1. In the **Placements/ Your placement** window, add a paywall, onboarding, or A/B test to display for default *All users* audience. To do this, click either the **Run paywall** or **Run A/B test** button, then select the desired paywall, onboarding, or A/B test from the dropdown list. 2. If you want to use more than one audience in the placement to create personalized paywalls tailored to different user groups, click the **Add audience** button and choose the desired user segment from the list. 3. Now add the paywall, onboarding, or A/B test to show for this audience. 4. Add as many audiences as you need. 5. If you have more than one audience, check that the audiences have the correct priorities. 6. Click the **Save and publish button**. --- # File: create-product.md --- --- title: "Create product" description: "Step-by-step guide on creating new subscription products in Adapty for better revenue management." --- No matter how you use Adapty, you need to create a product in the Adapty Dashboard and link products you've created in the app stores into it. Product creation in app stores is done separately from Adapty and involves defining details like price, duration, and free trials for your in-app purchases or subscriptions. Adapty will then use these settings to manage and analyze transactions in your app. Please check our guides on how to create products in stores: - [How to create a product in App Store](app-store-products) - [How to create a product in Google Play](android-products) After your products are set up in the stores, you are ready to add your products to the Adapty Dashboard. To add a new product to your app: 1. Open the **[Paywalls and Products](https://app.adapty.io/products)** section from the Adapty main menu, then select the **Products** tab. 2. Click the **Create product** button located in the top-right corner of the product list page. This action will initiate the process of creating a product within your app. Adapty supports all types of products: subscriptions, non-consumable \(including lifetime\), and consumable. 3. In the opened **Create product** window, enter the following data: - **Product name**: enter the name of the product to be used in the Adapty dashboard. The name is primarily for your reference, so feel free to choose a name that is most convenient for you to use across the Adapty Dashboard. - **Access Level**: Select the [access level](access-level) to which the product belongs. The access level is used to determine the features unlocked after purchasing the product. Note that this list contains only previously created access levels. The `premium` access level is created in Adapty by default, but you can also [add more access levels](access-level). - **Period**: select the duration of the subscription from the list. It should match the period configured in the App Store or Play Store. :::note By default, all the products have an **Uncategorized** period. Make sure to set the correct one, otherwise, there might be problems with granting access to your customers. If the product is not a subscription, use the following options: ::: - **Lifetime**: Use a lifetime period for the products that unlock the premium features of the app forever. - **Non-Subscriptions**: For the products that are not subscriptions and therefore have no duration, use non-subscriptions. These can be unlocked for additional features, consumable products, etc. - **Consumables**: Consumable items can be purchased multiple times. They could be used up during the life of the application. Examples are in-game currency and extras. Please consider that consumable products don’t affect access levels. 4. Click **Next**. 5. In the **Add stores info** window, configure the product information from each store: 1. **App Store:** - **App Store Product ID:** This unique identifier is used to access your product on devices. To obtain the product ID for the App Store, please follow the steps outlined in the [Product in App Store](app-store-products) page, where you'll find detailed instructions on how to create and retrieve the product ID. 2. **Play Store:** - **Google Play Product ID:** These are identifiers for the product from the Play Store. You need to provide at least one of them, but you can always add another one later if needed. To obtain the product ID for the Play Store, please follow the steps outlined in the [Product in Play Store](android-products) page, where you'll find detailed instructions on how to create and retrieve the product ID. - **Base Plan ID:** This ID is used to define the base plan for the product in the Play Store. When adding a subscription's Product ID on the Play Store you have to provide a Base Plan ID. A base plan defines the essential details of a subscription, encompassing the billing period, renewal type (auto-renewing or prepaid), and the associated price. Please note, that within Adapty, each combination of the same subscription and different base plans is treated as a separate product. - **Legacy fallback product**: A fallback product is used exclusively for apps using older versions of the Adapty SDK (versions 2.5 and below). By marking a product as backward compatible in the Google Play Console, Adapty can identify whether it can be purchased by older SDK versions. For this field please specify the value in the following format `:`. 3. **Stripe**: - **Stripe Product ID**: This is a unique identifier for a product in Stripe. - **Stripe Price ID**: In Stripe, price objects include more than just the price amount; they also cover tax behavior, volume tiers, and subscription intervals. Since a single product can have multiple prices, specify the correct price ID when creating a product in Adapty. Configuring product information for the App Store, Play Store, and Stripe ensures smooth integration and effective management of in-app purchases or subscriptions with Adapty. 6. (optional) You can add products from any custom store by clicking **Add custom store**. In the **Manage custom store info** window, you can select an existing custom store or add a new one and associate a product with it. Keep in mind that Adapty only tracks transactions from the App Store, Google Play, and Stripe. For custom stores, you’ll need to submit transactions using the Adapty server-side API [Set transaction method](ss-set-transaction). 7. Click **Create** to finalize the product creation. 8. (optional) You can [create offers](create-offer) for the product if needed. To add offers, click **Yes, add offers**. Otherwise, click **No, thanks**. 9. Finally, click **Save** to confirm the product creation. --- # File: create-service-account-key-file.md --- --- title: "Generate service account key file in the Google Play Console" description: "Learn how to create a service account key file for seamless integration with Adapty." --- To link your mobile app on the Play Store with Adapty, you'll need to generate special service account key files in the Google Play Console and upload them to Adapty. These files help secure your app and prevent unauthorized access. :::warning It usually takes at least 24 hours for your new service account to become active. However, there's a [hack](https://stackoverflow.com/a/60691844). After creating the service account in the [Google Play Console](https://play.google.com/apps/publish/), open any application and navigate to **Monetize** -> **Products** -> **Subscriptions/In-app products**. Edit the description of any product and save the changes. This should activate the service account immediately, and you can revert the changes afterward. ::: 1. Open the [**Service accounts**](https://console.cloud.google.com/iam-admin/serviceaccounts) section in the Google Play Console. Ensure you’ve selected the correct project. 2. In the window that opens, click **Add key** and choose **Create new key** from the dropdown menu. 3. In the **Create private key for [Your_project_name]** window, click **Create**. Your private key will be saved to your computer as a JSON file. You can find it using the file name provided in the **Private key saved to your computer** window. 4. In the **Create private key for Your_project_name** window, click the **Create** button. This action will save your private key on your computer as a JSON file. You can use the name of the file provided in the opened **Private key saved to your computer** window to locate it if needed. You’ll need this file when you [configure Google Play Store integration](google-play-store-connection-configuration). :::warning It usually takes at least 24 hours for your new service account to become active. However, there's a [hack](https://stackoverflow.com/a/60691844). After creating the service account in the [Google Play Console](https://play.google.com/apps/publish/), open any application and navigate to **Monetize** -> **Products** -> **Subscriptions/In-app products**. Edit the description of any product and save the changes. This should activate the service account immediately, and you can revert the changes afterward. ::: **What's next** - [Configure Google Play Store integration](google-play-store-connection-configuration) --- # File: create-service-account.md --- --- title: "Create service account in the Google Cloud Console" description: "Learn how to create a service account for secure API access in Adapty." --- For Adapty to automate data access, a service account is necessary in the Google Play Console. 1. Open [**IAM & Admin** - > **Service accounts**](https://console.cloud.google.com/iam-admin/serviceaccounts) section of the Google Cloud Console. Make sure you use the correct project. 2. In the **Service accounts** window, click the **Create service account** button. 3. In the **Service account details** sub-section of the **Create service account** window, enter the **Service Account Name** you want. We recommend including "Adapty" in the name to indicate the purpose of this account. The **Service account ID** will be created automatically. 4. Copy the service account email address and save it for future usage. 5. Click the **Create and continue** button. 6. In the **Select a role** drop-down list of the **Grant this service account access to project** sub-section, select **Pub/Sub -> Pub/Sub Admin**. This role is required to enable real-time developer notifications. 7. Click the **Add another role** button. 8. In a new **Role** drop-down list, select **Monitoring -> Monitoring Viewer**. This role is required to allow monitoring of the notification queue. 9. Click the **Continue** button. 10. Click the **Done** button without any changes. The **Service accounts** window opens. **What's next** - [Grant permissions to the service account in the Google Play Console](grant-permissions-to-service-account) --- # File: custom-media.md --- --- title: "Custom images and video in new Paywall Builder" description: "" --- You can set up any image or video on a paywall, but sometimes you might want to display a custom image or video that’s personalized for the user — for example, a video with their chosen avatar. That’s where custom media comes in. Custom media is an image or video that your app calls by ID from your code. It replaces a standard media file you’ve added to the paywall in the Paywall Builder. :::info To use this feature, update the Adapty SDK to version 3.7.0 or later. ::: ## Where can I use custom media? Anywhere you’d normally use a regular image or video: - As a hero image - As a hero video - As a regular icon - In a card background - In a carousel background ## How to use custom media? To set up custom media: 1. Select the **Use custom media ID** checkbox under the upload area. 2. Enter the media ID. For hero images and hero videos, IDs are predefined. 3. Upload a fallback image or video in the file upload section. 4. For more information, see [iOS](get-pb-paywalls.md), [Android](android-get-pb-paywalls.md), [Flutter](flutter-get-pb-paywalls.md), [React Native](react-native-get-pb-paywalls.md), and [Unity](unity-get-pb-paywalls.md) and call the media by its ID in your code. If the Adapty SDK version is below 3.7.0, or if the custom media ID isn't defined in your code, the fallback image or video will be displayed instead. --- # File: custom-store.md --- --- title: "Initial integration with other stores" description: "Adapty Initial Integration with App Store: A Quick Guide" --- We're thrilled to have you on board with Adapty! Our priority is to help you hit the ground running and achieve the best possible outcomes for your app. The initial integration is only needed for [App Store](initial_ios), [Google Play](initial-android), [Stripe](stripe), and [Paddle](paddle.md) since Adapty verifies your apps, products, and offers with these stores. Adapty doesn’t validate data with other app stores and does not process purchases made through them. However, you can still mark products sold through other stores for Adapty to grant access to paid content after a successful purchase, reflect transactions in your analytics, and share them via integrations.

:::important Make sure your backend processes the purchase and sends the transaction to Adapty using the [Adapty server-side API](getting-started-with-server-side-api). Adapty will only provide access, trigger a transaction event, send it to integrations, and reflect it in analytics after the transaction is received. ::: To mark a product as sold via a custom app store, select the app store when creating a product. If the store you need isn’t listed, here’s how to create one: 1. On the **Products** page, open the product you want to sell through a custom app store. 2. Choose the app store you want to sell through. If it’s not listed, click the **Create Custom Store** button. 3. Enter the store’s **Title** and **Store ID**. 4. Click the **Create store** button. If your backend is set up correctly, Adapty will receive product transactions from this custom store, reflect them in analytics, the [**Event Feed**](event-feed), and [integrations](https://app.adapty.io/integrations), and grant access accordingly. --- # File: custom-tags-in-legacy-paywall-builder.md --- --- title: "Custom tags in legacy Paywall Builder" description: "Implement custom tags in Adapty's legacy Paywall Builder to enhance subscription workflows." --- :::note Custom tags are only available on AdaptyUI SDK v.2.1.0 and higher ::: Custom tags are a feature designed to avoid creating separate paywalls for different situations. Imagine having a single paywall that adapts to different scenarios by incorporating specific user data. For instance, a simple greeting like "Hello!" can transform into a personalized message, such as "Hello, John!" or "Hello, Ann!" Various ways to use: - User’s email/name on the paywall - Current day of the week on the paywall to increase sales (as in “Happy Thursday“) - Custom properties of the products you're selling (name of the personalized fitness program, phone number in the VoIP app, etc) Custom tags enable you to create a consistent paywall for various situations, allowing your app's user interface to dynamically incorporate the relevant information. It's a practical solution for tailoring a paywall design for each specific user. :::warning Make sure to add fallbacks for every line with custom tags In some cases your app might not know what to replace a custom tag with: for example, if your Paywall is delivered to users on the older versions of AdaptyUI SDK. So when using custom tags, make sure to add fallback lines — they will be used to replace the lines containing unknown custom tags. Otherwise the user will see custom tags as code (``). ::: ## How to add a custom tag to a paywall Every text line you see in Paywall Builder can have one or more custom tags. To add a custom tag to a line: 1. Enter the custom tag you want in the format `` or simply type an opening angle bracket (\<) in the text line followed by the custom tag you need. The system will then offer you the tag in the correct format. Please pay attention that: - In the Adapty Paywall Builder, custom tags are wrapped in angle brackets (``) while in mobile app code, you should refer to them directly (`CUSTOM_TAG`). - Custom tags are case-sensitive. - Custom tags can't overlap with any of the [Tag Variables](paywall-builder-tag-variables) reserved for product info in Adapty. 2. After entering the custom tag, make sure to enter the fallback line. The fallback is the text displayed in your app if it does not know about a particular custom tag. This ensures that users won't see the custom tag as code; instead, they'll see the designated fallback text. Please note that the fallback replaces the entire line containing the custom tag ## How to use custom tags in your mobile app To use custom tags in your mobile app, you need to create a `tagResolver` object. This is a dictionary/map containing custom tags and the string values to replace them with when rendering the paywall in your app. Here's an example: ```swift showLineNumbers title="Swift" let tagResolver = [ "USERNAME": "John", ] ``` ```kotlin showLineNumbers title="Kotlin" val customTags = mapOf("USERNAME" to "John") val tagResolver = AdaptyUiTagResolver { tag -> customTags[tag] } ``` ```java showLineNumbers title="Java" Map customTags = new HashMap<>(); customTags.put("USERNAME", "John"); AdaptyUiTagResolver tagResolver = customTags::get; ``` ```typescript showLineNumbers let customTags: Record = { "USERNAME": "John" } //and then you can pass it to createPaywallView as follows: view = await createPaywallView(paywall, { customTags }) ``` In this example, `USERNAME` is a custom tag that you entered in the Adapty dashboard while designing a paywall as ``. The `tagResolver` ensures that when your app encounters this custom tag, it dynamically replaces it with the specified value, in this case, `John`. We recommend to create and populate the `tagResolver` right before presenting your paywall. Once it is created, pass it over to the AdaptyUI method used for presenting. Read more on how to present paywalls on [iOS](ios-present-paywalls), [Android](android-present-paywalls), [Flutter](flutter-present-paywalls), [React Native](react-native-present-paywalls), or [Unity](unity-present-paywalls). --- # File: custom-tags-in-paywall-builder.md --- --- title: "Custom tags in Paywall Builder" description: "Learn how to use custom tags in Adapty's Paywall Builder to personalize user experiences and optimize conversions." --- 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!" :::warning This section describes the new Paywall Builder, compatible with iOS, Android, and React Native SDKs version 3.0 or higher and Flutter and Unity SDKs version 3.3.0 or higher. For information on the legacy Paywall Builder compatible with Adapty SDK v2.x or earlier, see [Custom tags in legacy Paywall Builder](custom-tags-in-legacy-paywall-builder). ::: 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 (``). ::: ## How to add a custom tag to a paywall You can add one or more custom tags to any text line in Paywall Builder. To add a custom tag: 1. Enter the custom tag in the format `` or type an opening angle bracket (\<) in the text line. The system will then suggest the tag in the correct format. A few things to keep in mind: - In the Adapty Paywall Builder, custom tags are wrapped in angle brackets (``), but in your app’s code, they should be referenced directly (CUSTOM_TAG). - Custom tags are case-sensitive. - Custom tags can’t overlap with any of the [Tag Variables](paywall-builder-tag-variables) reserved for product info in Adapty. 2. After adding the custom tag, make sure to enter a fallback line. This fallback text will appear in your app if it doesn’t recognize a particular custom tag, ensuring users won’t see the tag displayed as code. The fallback text replaces the entire line containing the custom tag. ## How to use custom tags in your mobile app 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: ```swift showLineNumbers let tagResolver = [ "USERNAME": "John", ] let paywallConfiguration = try await AdaptyUI.getPaywallConfiguration( forPaywall: paywall, tagResolver: tagResolver // or any other AdaptyTagResolver protocol implementation ) ``` ```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; ``` ```dart showLineNumbers final customTags = { 'USERNAME': 'John', }; try { final view = await AdaptyUI().createPaywallView( paywall: paywall, customTags: customTags, ); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ```csharp showLineNumbers var parameters = new AdaptyUICreateViewParameters() .SetCustomTags( new Dictionary { { "CUSTOM_TAG_NAME", "John Appleseed" } } ) AdaptyUI.CreateView(paywall, parameters, (view, error) => { // handle the result }); ``` ```typescript showLineNumbers let customTags: Record = { "USERNAME": "John" } //and then you can pass it to createPaywallView as follows: view = await createPaywallView(paywall, { customTags }) ``` 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. You can read more about presenting paywalls on [iOS](ios-present-paywalls), [Android](android-present-paywalls), [Flutter](flutter-present-paywalls), [React Native](react-native-present-paywalls), or [Unity](unity-present-paywalls). --- # File: customize-onboardings-with-remote-config.md --- --- title: "Customize onboarding with remote config" description: "Customize your onboarding with remote config in Adapty for better targeting." --- The onboarding remote config is a tool that provides flexible configuration options. It allows the use of custom JSON payloads to tailor your onboardings precisely. With it, you can define various parameters such as titles, images, fonts, colors, and more. For example, you can use remote configs to send additional metadata. Before you start customizing an onboarding, [create an onboarding](create-onboarding.md). To start customizing an onboarding using the remote config: 1. Open the onboarding from the **Onboardings** and click **Edit onboarding**. 2. Switch to the **Remote config** tab. Remote config has two views: - [Table](customize-paywall-with-remote-config#table-view-of-the-remote-config) - [JSON](customize-paywall-with-remote-config#json-view-of-the-remote-config) Both the **Table** and **JSON** views include the same configuration elements. The only distinction is a matter of preference, with the sole difference being that the table view offers a context menu, which can be helpful for correcting localization errors. You can switch between views by clicking on the **Table** or **JSON** tab whenever necessary. Whatever view you've chosen to customize your onboarding, you can later access this data from SDK using the `remoteConfig` property of `AdaptyOnboarding`, and make some adjustments to your onboarding. You can combine different options and create your own. ### JSON view of the remote config In the **JSON** view of the remote config, you can enter any JSON-formatted data: ### Table view of the remote config If you don't often work with code and need to correct some JSON values, Adapty has the **Table** view for you. It is a copy of your JSON in a table format that is easy to read and understand. Color coding helps to recognize different data types. To add a key, click **Add row**. We automatically check the values and types mapping and show an alert if your corrections may lead to an invalid JSON. --- # File: customize-paywall-with-remote-config.md --- --- title: "Design paywall with remote config" description: "Customize your paywall with remote config in Adapty for better targeting." --- The Paywall Remote Config is a powerful tool that provides flexible configuration options. It allows the use of custom JSON payloads to tailor your paywalls precisely. With it, you can define various parameters such as titles, images, fonts, colors, and more.
Before you start customizing a paywall (Click to Expand) 1. [Create a product](create-product). 2. [Create a paywall and add the product to it](create-paywall).
To start customizing a paywall using the remote config: 1. Open the [**Paywalls**](https://app.adapty.io/paywalls) section in the Adapty main menu. 2. Click the paywall to open it. 3. Switch to the **Remote config** tab. Remote config has 2 views: - [Table](customize-paywall-with-remote-config#table-view-of-the-remote-config) - [JSON](customize-paywall-with-remote-config#json-view-of-the-remote-config) Both the **Table** and **JSON** views include the same configuration elements. The only distinction is a matter of preference, with the sole difference being that the table view offers a context menu, which can be helpful for correcting localization errors. You can switch between views by clicking on the **Table** or **JSON** tab whenever necessary. Whatever view you've chosen to customize your paywall, you can later access this data from SDK using the`remoteConfig` or `remoteConfigString` properties of `AdaptyPaywall`, and make some adjustments to your paywall. You can also programmatically update remote config values using the [server-side API](ss-update-paywall) to dynamically modify paywall configurations without manual dashboard updates. Here are some examples of how you can use a remote config. ```json showLineNumbers { "screen_title": "Today only: Subscribe, and get 7 days for free!" } # Test titles or others texts ``` ```json showLineNumbers { "background_image": "https://adapty.io/media/paywalls/bg1.webp" } # Test images on your paywall ``` ```json showLineNumbers { "font_family": "San Francisco", "font_size": 16 } # Test fonts ``` ```json showLineNumbers { "subscribe_button_color": "purple" } # Test colors of buttons, texts etc. ``` ```json showLineNumbers { "photo_gallery": "https://adapty.io/media/paywalls/link-to-html-snippet.html" } # Any HTML code that can be displayed on the paywall ``` ```json showLineNumbers { "hard_paywall": true } # By setting it to true, you disalow skipping paywall without subscribing # You have to handle this logic in your app ``` ```json showLineNumbers { "title": { "en": "Try for free!", "es": "¡Prueba gratis!", "ru": "Попробуй бесплатно!" } } ``` You can combine different options, and make up your own. This way you can test different titles, texts, images, fonts, colors, and so on. ### JSON view of the remote config In the **JSON** view of the remote config, you can enter any JSON-formatted data: ### Table view of the remote config If it's not common for you to work with code and there is a need to correct some values of the JSON, Adapty has the **Table** view for you. It is a copy of your JSON in the format of a table that is easy to read and understand. Color coding helps to recognize different data types. To add a key, click the **Add row** button. We automatically check the values and types mapping and show an alert if your corrections may lead to an invalid JSON. Additional row options are mostly useful for [paywall localisations](add-remote-config-locale): Now it's time to [create a placement](create-placement) and add the paywall to it. After that, you can [iOS](present-remote-config-paywalls.md), [Android](present-remote-config-paywalls-android.md), [Flutter](present-remote-config-paywalls-flutter.md), [React Native](present-remote-config-paywalls-react-native.md), and [Unity](present-remote-config-paywalls-unity.md) in your mobile app. --- # File: delete-placement.md --- --- title: "Delete placement" description: "Find out how to delete a placement in Adapty without affecting your paywall performance." --- A [Placement](placements) designates a specific location within your mobile app where a paywall, onboarding, or A/B test can be displayed. :::danger Although you have the option to delete any placement, it is critical to ensure that you don't delete a placement that is actively used in your mobile app. Deleting an active paywall placement will result in a local fallback paywall being permanently shown if you've [set it up](fallback-paywalls), and you won't be able to ever replace it with a dynamic paywall in released app versions. ::: To delete an existing placement: 1. Go to **[Placements](https://app.adapty.io/placements)** from the Adapty main menu. If you want to delete a placement for onboarding, switch to the **Onboardings** tab. 2. Click the **3-dot** button next to the placement and select the **Delete** option. 3. In the opened **Delete placement** window, enter the product name you're about to delete. 4. Click the **Delete forever** button to confirm the deletion. --- # File: delete-product.md --- --- title: "Delete product" description: "Find out how to delete a subscription product in Adapty without disrupting your app's revenue flow." --- You can only delete products that are not used in paywalls. To delete the product: 1. Open the **[Paywalls and Products](https://app.adapty.io/products)** section from the Adapty main menu, then select the **Products** tab. 2. Click the **3-dot** button next to the product and select the **Delete** option. 2. In the **Delete product** window, enter the product name you're about to delete. 3. Click the **Delete forever** button to confirm the deletion. --- # File: design-onboarding.md --- --- title: "Design onboardings" description: "Create meaningful onboardings." --- The no-code mobile app onboarding builder is a powerful and customizable tool that will help you provide your users with the best onboarding experience. You don't even need to be a developer or designer to get a great result. ## Onboarding screens The onboarding flow consists of several screens that you add and design. Users will tap the button to navigate between them. :::tip If some of your users need a slightly different flow (for example, in your fitness app, you might want to show different 'goal' pictures based on the user's gender), you don't need to create separate onboardings. Instead, you can [make some screens hidden by default and displayed only for some scenarios](onboarding-user-engagement.md). ::: ## Onboarding elements The onboarding elements are displayed on the left in the order they are displayed. Click **Add** at the top right to add a new element. There are the following groups of elements you can add: - **Containers**: Containers allow you to set up a flexible layout. For example, if you want to add a two-column text, you need to add **Columns** and then drag two text blocks into **Columns** on the left pane. Or, if you are adding a carousel, you'll need to add images to the **Media** elements inside. - **Typography**: Add pre-formatted text blocks and configure their look as needed. - **Media & Display**: Except for images and videos, you can add animated charts that demonstrate your app value and encourage users. The **supported video formats** are MP4 and WebM. The **maximum media file size** is 15 MB. If you want to add an unsupported animated element (like Lottie), you can convert it to a video (for example, with [this tool](https://www.lottielab.com/lottie-to-video)) and embed it as a video. - **Quiz**: Create short questionnaires with text and image options to customize the onboarding experience and get to know your users better. - **Inputs**: Collect your users' data. - **Buttons**: Buttons let your users navigate between screens, close the onboarding or move to the paywall. You can also add glossy or moving buttons to attract user's attention and convert their install to a purchase. - **Loaders**: Animated loaders keep users engaged during the process. - **User engagement**: Add testimonials, user email lists and countdowns. :::note As a part of the **Media & Display** group, you can also add custom HTML code if the provided customization options are not enough. However, custom HTML elements are neither preloaded nor cached, so it is recommended to use **Raw HTML** only for small, lightweight elements. ::: ### Element ID and action ID If you want to use a button for custom actions, assign it an **action ID** and then use it in your source code. Action IDs let you handle different buttons with the same action ID in the same way. If you want to process user input in a specific field (e.g., save their age or email), assign it an **element ID** and then use it in your source code to associate questions with answers. Element IDs can be used only once in your onboarding. ## Customization options You have the following customization options in the builder: - **Styles** tab: Adjust the element's look. - **Element** tab: Set the element’s attributes, such as visibility, actions for pressing buttons or other properties unrelated to the element's look. - **Screen** tab: Set up the general screen configuration, such as a header or displaying a screen counter. ## Copy screens and elements If you've created an onboarding and want to reuse parts of it, or if you want to make slight changes and run A/B tests, you can copy one or more screens from one onboarding to another. To copy screens, open the onboarding builder and either: - Right-click a single screen and select **Copy** or select multiple screens with `Shift`, right-click, and choose **Copy** - Select the desired screen(s) and press `Ctrl+C` (Windows) or `⌘+C` (Mac) You can also copy individual elements or text blocks, either within the same onboarding or between different onboardings. ## Copy screens from web-to-app funnels If you use web-to-app funnels created in [FunnelFox](https://funnelfox.com/) and want to use screens from funnels in onboardings, you can quickly do it by copying screens in the funnel builder and pasting them in the onboarding builder: 1. In the FunnelFox funnel builder, right-click a screen and select **Copy**, or select the screen and press `Ctrl+C`/`⌘+C`. 2. Open the onboarding builder. 3. Right-click the screen where you want to insert the copied screen and select **Paste**, or select it and press `Ctrl+V`/`⌘+V`. The copied screen will be inserted below the selected screen. --- # File: duplicate-paywalls.md --- --- title: "Duplicate paywall" description: "Learn how to manage duplicate paywalls and optimize paywall performance in Adapty." --- If you need to make small changes to an existing paywall in Adapty, especially when it's already being used in your mobile app and you don't want to mess up your analytics, you can simply duplicate it. You can then use these duplicates to replace the original paywalls in some or all placements as needed. This creates a copy of the paywall with all its details, like its name, products, and any promotions. The new paywall will have "Copy" added to its name so you can easily tell it apart from the original. To duplicate a paywall in the Adapty dashboard: 1. Open the [**Paywalls**](https://app.adapty.io/paywalls) section in the Adapty main menu. The paywall list page in the Adapy dashboard provides an overview of all the paywalls present in your account. 2. Click the **3-dot** button next to the paywall and select the **Duplicate** option. 3. Adjust the new paywall and click the **Save** button. 4. Adapty will prompt you to replace the original paywalls with their duplicates in placements if the original paywall is currently used in any placement. If you choose **Create and replace original**, the new paywalls will immediately go **Live**. Alternatively, you can create them as new paywalls in the **Draft** state and add them to placements later. --- # File: edit-placement.md --- --- title: "Edit placement" description: "Learn how to edit placements in Adapty to optimize paywall visibility and user engagement." --- A [Placement](placements) designates a specific location within your mobile app where a paywall, onboarding, or A/B test can be displayed. For example, a subscription choice might appear in a startup flow, while a consumable product (such as golden coins) could be presented when a user runs out of coins in a game. You have the flexibility to showcase the same or different paywalls, onboardings, or A/B tests across multiple placements or user segments, called audiences in Adapty. To edit an existing placement: 1. Go to **[Placements](https://app.adapty.io/placements)** from the Adapty main menu. If you want to edit a placement for onboarding, switch to the **Onboardings** tab. 2. Click the placement you want to edit. 3. Click **Edit placement** at the top right. 4. Make the changes you need. For more details on the options in this window, please read the [Create placement](create-placement) section. 5. Click the **Save and publish** button to confirm the changes. --- # File: edit-product.md --- --- title: "Edit product" description: "Modify and manage your subscription products in Adapty for better revenue tracking." --- In Adapty you can combine similar products that you have in App Store and Play Store in a single internal [Product](product). This allows you to use a single Adapty product across all platforms, instead of using each vendor's products. :::warning While you have the option to edit any product, it's crucial to ensure that making changes to products already used in live paywalls doesn't lead to discrepancies in your analytics. Editing period, access level, App Store Product ID, and Play Store Product ID is not recommended because it may affect analytics clarity. Only edit them if you made a mistake, like setting the wrong period or typo in the product ID. If you no longer use the product and want to replace it with another one, we strongly advise you to create a new product and update Paywalls and A/B tests accordingly. ::: To edit the product: 1. Open the **[Paywalls and Products](https://app.adapty.io/products)** section from the Adapty main menu, then select the **Products** tab. 2. Click the **3-dot** button next to the product and select the **Edit** option. 3. In the opened **Edit** window, make the changes you need. For more details on the options in this window, please read the [Create product](create-product) section. 4. Click the **Save** button to confirm the changes. --- # File: enable-app-store-server-notifications.md --- --- title: "Enable App Store server notifications" description: "Enable App Store server notifications to track subscription events in real time." --- Setting up App Store server notifications is crucial for ensuring data accuracy as it enables you to receive updates instantly from the App Store, including information on refunds and other events. 1. Copy the **URL for App Store server notification** in the Adapty Dashboard. 2. Open [App Store Connect](https://appstoreconnect.apple.com/apps). Select your app and proceed to **General** → **App Information** section, **App Store Server Notifications** subsection. 3. Paste the copied **URL for App Store server notification** into the **Production Server URL** and **Sandbox Server URL** fields. ## Raw events forwarding Sometimes, you might still want to receive raw S2S events from Apple. To continue receiving them while using Adapty, just add your endpoint to the **URL for forwarding raw Apple events** field, and we'll send raw events as-is from Apple. **What's next** Set up the Adapty SDK for: - [iOS](sdk-installation-ios) - [Flutter](sdk-installation-flutter) - [React Native](sdk-installation-reactnative) - [Unity](sdk-installation-unity) --- # File: enable-real-time-developer-notifications-rtdn.md --- --- title: "Enable Real-time developer notifications (RTDN) in Google Play Console" description: "Stay informed about critical events and maintain data accuracy by enabling Real-time Developer Notifications (RTDN) in the Google Play Console for Adapty. Learn how to set up RTDN to receive instant updates about refunds and other important events from the Play Store" --- Setting up real-time developer notifications (RTDN) is crucial for ensuring data accuracy as it enables you to receive updates instantly from the Play Store, including information on refunds and other events. 1. Open the [**App settings**](https://app.adapty.io/settings/android-sdk) from the Adapty top menu. 2. Copy the contents of the **Enable Pub/Sub API** field next to the **Google Play RTDN topic name** title.

:::note If the contents of the **Enable Pub/Sub API** field have a wrong format (the correct format starts with `projects/...`), refer to the [Fixing incorrect format in Enable Pub/Sub API field](enable-real-time-developer-notifications-rtdn#fixing-incorrect-format-in-enable-pubsub-api-field) section for help. ::: 3. Open the [Google Play Console](https://play.google.com/console/), choose your app, and scroll down the left menu to find **Monetize** -> **Monetization setup**. 4. In the **Google Play Billing** section, select the **Enable real-time notifications** check-box. 5. Paste the contents of the **Enable Pub/Sub API** field you've copied in the Adapty **App Settings** into the **Topic name** field. 6. Click the **Save changes** button in the Google Play Console. ## Fixing incorrect format in Enable Pub/Sub API field If the contents of the **Enable Pub/Sub API** field are in the wrong format (the correct format starts with `projects/...`), follow these steps to troubleshoot and resolve the issue: ### 1. Verify API Enablement and Permissions Carefully ensure that all required APIs are enabled, and permissions are correctly granted to the service account. Even if you've already completed these steps, it’s important to go through them again to make sure no sub-step was missed. Repeat the steps in the following sections: 1. [Enable Developer APIs in Google Play Console](enabling-of-devepoler-api) 2. [Create service account in the Google Cloud Console](create-service-account) 3. [Grant permissions to service account in the Google Play Console](grant-permissions-to-service-account) 4. [Generate service account key file in the Google Play Console](create-service-account-key-file) 5. [Configure Google Play Store integration](google-play-store-connection-configuration) ### 2. Adjust Domain Policies Change the **Domain restricted contacts** and **Domain restricted sharing** policies: 1. Open the [Google Cloud Console](https://console.cloud.google.com/) and select the project where you created the service account to manage your app. 2. In the **Quick Access** section, choose **IAM & Admin**. 3. In the left pane, choose **Organization Policies**. 4. Find the **Domain restricted contacts** policy. 5. Click the ellipsis button in the **Actions** column and choose **Edit policy**. 6. In the policy editing window: 1. Under **Policy source**, select the **Override parent's policy** radio-button. 2. Under **Policy enforcement**, select the **Replace** radio button. 3. Under **Rules**, click the **ADD A RULE** button. 4. Under **New rule** -> **Policy values**, choose **Allow All**. 5. Click **SET POLICY**. 7. Repeat steps 4-6 for the **Domain restricted sharing** policy. Finally, recreate the contents of the **Enable Pub/Sub API** field next to the **Google Play RTDN topic name** title. The field will now have the correct format. Make sure to switch the **Policy source** back to **Inherit parent's policy** for the updated policies once you've successfully enabled Real-time Developer Notifications (RTDN). ## Raw events forwarding Sometimes, you might still want to receive raw S2S events from Google. To continue receiving them while using Adapty, just add your endpoint to the **URL for forwarding raw Google events** field, and we'll send raw events as-is from Google. --- **What's next** Set up the Adapty SDK for: - [Android](sdk-installation-android) - [Flutter](sdk-installation-flutter) - [React Native](sdk-installation-reactnative) - [Unity](sdk-installation-unity) --- # File: enabling-of-devepoler-api.md --- --- title: "Enable Developer APIs in Google Play Console" description: "Enable Adapty's Developer API to automate and streamline subscription management in your app." --- If your mobile app is available in the Play Store, activating Developer APIs is crucial for integrating it with Adapty. This step ensures seamless communication between your app and our platform, facilitating automated processes and real-time data analysis to optimize your subscription model. The following APIs should be enabled: - [Google Play Android Developer API](https://console.cloud.google.com/apis/library/androidpublisher.googleapis.com) - [Google Play Developer Reporting API](https://console.cloud.google.com/apis/library/playdeveloperreporting.googleapis.com) - [Cloud Pub/Sub API](https://console.cloud.google.com/marketplace/product/google/pubsub.googleapis.com) If your app isn't distributed via the Play Store, you can skip this step. However, if you do sell through the Play Store, you can delay this step for now, though it's crucial for Adapty's basic functionality. After completing the onboarding process, you can configure the application store settings in the **App settings** section. Here's how to enable Developer APIs in the Google Play Console: 1. Open the [Google Cloud Console](https://console.cloud.google.com/). 2. In the top-left corner of the Google Cloud window, select the project you wish to use or create a new one. Ensure you use the same Google Cloud project until you upload the service account key file to Adapty. 3. Open the [**Google Play Android Developer API**](https://console.cloud.google.com/apis/library/androidpublisher.googleapis.com) page. 4. Click the **Enable** button and wait for the status **Enabled** to show. This means the Google Android Developer API is enabled. 5. Open the [**Google Play Developer Reporting API**](https://console.cloud.google.com/apis/library/playdeveloperreporting.googleapis.com) page. 6. Click the **Enable** button and wait for the status **Enabled** to show. 7. Open the [**Cloud Pub/Sub API**](https://console.cloud.google.com/marketplace/product/google/pubsub.googleapis.com) page. 8. Click the **Enable** button and wait for the status **Enabled** to show. Developer APIs are enabled. You can recheck it on the [**APIs & Services**](https://console.cloud.google.com/apis/dashboard) page of the Google Cloud Console. Scroll the page down, and validate the table at the bottom of the page contains all 3 APIs: - Google Play Android Developer API - Google Play Developer Reporting API - Cloud Pub/Sub API **What's next** - [Create a service account in the Google Cloud Console](create-service-account) --- # File: error-handling-on-flutter-react-native-unity.md --- --- title: "Handle errors in Flutter SDK" description: "Handle errors in Flutter SDK." --- Every error is returned by the SDK is `AdaptyErrorCode`. Here is an example: :::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. ::: ```javascript showLineNumbers try { final result = await adapty.makePurchase(product: product); } on AdaptyError catch (adaptyError) { if (adaptyError.code == AdaptyErrorCode.paymentCancelled) { // Cancelled } } catch (e) { } ``` ##  System StoreKit codes | Error | Code | Solution | |-----|----|-----------| | [unknown](https://developer.apple.com/documentation/storekit/skerror/code/unknown) | 0 | Error code indicating that an unknown or unexpected error occurred.
Retry or see the [Other issues](#other-issues) section. | | [clientInvalid](https://developer.apple.com/documentation/storekit/skerror/code/clientinvalid) | 1 | This error code indicates that the client is not allowed to perform the attempted action. | | [paymentCancelled](https://developer.apple.com/documentation/storekit/skerror/code/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](https://developer.apple.com/documentation/storekit/skerror/code/paymentinvalid) | 3 | This error indicates that one of the payment parameters was not recognized by the App Store. | | [paymentNotAllowed](https://developer.apple.com/documentation/storekit/skerror/code/paymentnotallowed) | 4 | This error code indicates that the user is not allowed to authorize payments. | | [storeProductNotAvailable](https://developer.apple.com/documentation/storekit/skerror/code/storeproductnotavailable) | 5 | This error code indicates that the requested product is not available in the store.
Try re-installing the app. | | [cloudServicePermissionDenied](https://developer.apple.com/documentation/storekit/skerror/code/cloudservicepermissiondenied) | 6 | This error code indicates that the user has not allowed access to Cloud service information. | | [cloudServiceNetworkConnectionFailed](https://developer.apple.com/documentation/storekit/skerror/code/cloudservicenetworkconnectionfailed) | 7 | This error code indicates that the device could not connect to the network. | | [cloudServiceRevoked](https://developer.apple.com/documentation/storekit/skerror/code/cloudservicerevoked/) | 8 | This error code indicates that the user has revoked permission to use this cloud service. | | [privacyAcknowledgementRequired](https://developer.apple.com/documentation/storekit/skerror/code/privacyacknowledgementrequired) | 9 | This error code indicates that the user has not yet acknowledged Apple’s privacy policy. | | [unauthorizedRequestData](https://developer.apple.com/documentation/storekit/skerror/code/unauthorizedrequestdata) | 10 | This error code indicates that the app is attempting to use a property for which it does not have the required entitlement. | | [invalidOfferIdentifier](https://developer.apple.com/documentation/storekit/skerror/code/invalidofferidentifier) | 11 |

The offer [`identifier`](https://developer.apple.com/documentation/storekit/skpaymentdiscount/3043528-identifier) is not valid. For example, you have not set up an offer with that identifier in the App Store, or you have revoked the offer.

Make sure you set up desired offers in AppStore Connect and pass a valid offer identifier.

| | [invalidSignature](https://developer.apple.com/documentation/storekit/skerror/code/invalidsignature) | 12 | This error code indicates that the signature in a payment discount is not valid. | | [missingOfferParams](https://developer.apple.com/documentation/storekit/skerror/code/missingofferparams) | 13 | This error code indicates that parameters are missing in a payment discount. | | [invalidOfferPrice](https://developer.apple.com/documentation/storekit/skerror/code/invalidofferprice/) | 14 | This error code indicates that the price you specified in App Store Connect is no longer valid. Offers must always represent a discounted price. | ## Custom Android codes | Error | Code | Solution | |-----|----|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | adaptyNotInitialized | 20 | You need to properly configure Adapty SDK by `Adapty.activate` method. Learn how to do it [for Flutter]( sdk-installation-flutter#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 | Solution | |-----------------------------------------------------------------------------------------------------|------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | noProductIDsFound | 1000 |

This error 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, ignore it.

If you’re encountering this error, follow the steps in the [Fix for Code-1000 `noProductIDsFound` error](InvalidProductIdentifiers-flutter) section.

| | noProductsFound | 1001 | This error indicates that the product requested for purchase is not available in the store. | | productRequestFailed | 1002 | Unable to fetch available products at the moment. | | cantMakePayments | 1003 | In-app purchases are not allowed on this device. See the troubleshooting [guide](cantMakePayments-flutter). | | noPurchasesToRestore | 1004 | This error indicates that the App Store did not find the purchase to restore. | | [cantReadReceipt](https://developer.apple.com/documentation/storekit/skerror/code/paymentcancelled) | 1005 |

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

In the sandbox, you won't have a valid receipt file until you actually make a purchase, so make sure you do one before accessing it. During sandbox testing also make sure you signed in on a device with a valid Apple sandbox account.

| | productPurchaseFailed | 1006 | Product purchase failed. The StoreKit error unrelated to Adapty. Try using a new [sandbox profile](test-purchases-in-sandbox). If it doesn't help, contact the Apple support. | | missingOfferSigningParams | 1007 |

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.

| | 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 | Solution | | :------------------- | :--- |:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | notActivated | 2002 | The Adapty SDK is not activated. You need to properly [configure Adapty SDK](sdk-installation-flutter#configure-adapty-sdk) using the `Adapty.activate` method. | | badRequest | 2003 | Bad request.
Ensure you've completed all the steps required to [integrate with the App Store](app-store-connection-configuration). | | serverError | 2004 | Server error.
Try again after some time. If the issue is not resolved, contact the Adapty support team. | | networkFailed | 2005 | The error indicates issues with the network connection on the user's device.
Try disabling VPN or switching to WiFi from a cellular network or vice versa. | | decodingFailed | 2006 | This error indicates that response decoding failed.
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. | | encodingFailed | 2009 | This error indicates that request encoding failed. | | analyticsDisabled | 3000 | We can't handle analytics events, since you've [opted it out](analytics-integration#disabling-external-analytics-for-a-specific-customer). | | wrongParam | 3001 | This error indicates that some of your parameters are not correct.
If you're using the Adapty paywall builder and can't display a paywall because of this error, toggle on **Show on device** in the paywall builder.
Another possible reason for this issue is that the local [fallback](fallback-paywalls) file version doesn't match the SDK version. Download a new file in the dashboard. | | activateOnceError | 3005 | It is not possible to call `.activate` method more than once. | | profileWasChanged | 3006 | The user profile was changed during the operation.
This error can occur when you call `identify`, and then call another method before `identify` succeds. To avoid it, wait untill `identify` suceeds before calling other methods. | | unsupportedData | 3007 | This error indicates that the data format is not supported by the SDK. | | persistingDataError | 3100 | It was an error while saving data. | | fetchTimeoutError | 3101 | This error indicates that the fetch operation timed out. | ## 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 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-flutter#logging) and share logs with the team. You can also attach relevant code snippets. --- # File: eu-cluster.md --- --- title: "Adapty on EU servers" description: "Understand Adapty’s EU cluster and its data storage policies." no_index: true --- If legal requirements mandate EU-based servers, Adapty provides an EU cluster option. Follow these steps: 1. Configure Adapty SDK for the EU region. 2. Register with Adapty via the EU-specific registration link. 3. Log in to Adapty through the EU-specific login link. After setup, you can use the Adapty Dashboard as usual at `app.adapty.io`. ## Step 1. Configure Adapty SDK for EU region Install Adapty SDK 3.0.3 or later as described in [Adapty SDK Installation & Configuration](sdk-installation-android). During configuration, add the EU cluster as follows: Include the `backendBaseUrl` parameter in your configuration: ```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) // highlight-next-line .with(backendBaseUrl: URL(string: "https://api-eu.adapty.io/api/v1")!) 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 // highlight-next-line .with(backendBaseUrl: URL(string: "https://api-eu.adapty.io/api/v1")!) Adapty.activate(with: configurationBuilder.build()) { error in // handle the error } } var body: some Scene { WindowGroup { ContentView() } } } ``` Parameters: | Parameter | Description | | ------------------ | ------------------------------------------------------------ | | **backendBaseUrl** | Use the `URL(string: "https://api-eu.adapty.io/api/v1")!` value to connect your app to Adapty’s European servers. | Install Adapty SDK 3.0.4 or later as described in [Adapty SDK Installation & Configuration](sdk-installation-android). During configuration, add the EU cluster as follows: Use the `.withServerCluster` method in your configuration: ```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 // highlight-next-line .withServerCluster(AdaptyConfig.ServerCluster.EU) .build() ) } ``` ```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 // highlight-next-line .withServerCluster(AdaptyConfig.ServerCluster.EU) .build() ); } ``` Added method: | Method | Description | | --------------------- | ------------------------------------------------------------ | | **withServerCluster** | Pass the value `AdaptyConfig.ServerCluster.EU` to it to connect your app to Adapty’s European servers. | ## Step 2. Register with EU cluster To register your company in the EU cluster, use https://app.adapty.io/eu-registration. Once registered, you can [add employees](members-settings#how-to-add-a-member) in the Adapty Dashboard as usual. ## Step 3: Log in with EU-specific link Direct your team to log in via https://app.adapty.io/eu-login. After logging in, they can use https://app.adapty.io for regular dashboard access. :::warning New users or existing users logging in from a new device must use https://app.adapty.io/eu-login for the first login. ::: This should help ensure you comply with EU data regulations while using Adapty!x --- # File: event-feed.md --- --- title: "Event feed" description: "Monitor and analyze user activity with Adapty’s event feed." --- Event feed allows you to visually track [Events](events) generated by Adapty and check the status of their export to 3rd-party integrations, including the webhook. :::warning Keep in mind that transactions created using the [server-side API (version 1)](server-side-api-specs-legacy#requests) do not appear in the **Event Feed**. To ensure they are included, use the [server-side API (version 2)](ss-set-transaction) instead. :::

:::note AppsFlyer, Facebook Ads, and Branch sending status could be inaccurate because they do not always return errors when they occur. ::: To view the profile of the user who has initiated the transaction, click the **View Profile** button in the event details. --- # File: event-flows.md --- --- title: "Event flows" description: "Discover detailed schemes of subscription event flows in Adapty. Learn how subscription events are generated and sent to integrations, helping you track key moments in your customers' journeys." --- In Adapty, you'll receive various subscription events throughout a customer’s journey in your app. These subscription flows outline common scenarios to help you understand the events that Adapty generates as users subscribe, cancel, or reactivate subscriptions. Keep in mind that Apple processes subscription payments several hours before the actual start/ renewal time. In the flows below, we show both the subscription start/ renewal and the charge happening at the same time to keep the diagrams clear. Also, events related to the same action occur simultaneously and may appear in your **Event Feed** in any order, which might differ from the sequence shown in our diagrams. ## Subscription Lifecycle ### Initial Purchase Flow This flow happens when a customer buys a subscription for the first time without a trial. In this situation, the following events are created: - **Subscription started** - **Access level updated** to grant access to the user When the subscription renewal date comes, the subscription is renewed. In this case, the following events are created: - **Subscription renewal** to start a new period of the subscription - **Access level updated** to update the subscription expiration date, extending access for another period Situations when the payment is not successful or when the user cancels the renewal are described in [Billing Issue Outcome Flow](event-flows#billing-issue-outcome-flow) and [Subscription Cancellation Flow](event-flows#subscription-cancellation-flow) ,respectively. ### Subscription Cancellation Flow When a user cancels their subscription, the following events are created: - **Subscription renewal canceled** to indicate that the subscription remains active until the end of the current period, after which the user will lose access - The **Access level updated** event is created to disable auto-renewal for the access Once the subscription ends, the **Subscription expired (churned)** event is triggered to mark the end of the subscription. If a refund is approved, the following event replaces **Subscription expired (churned)**: - **Subscription refunded** to end the subscription and provide details about the refund For Stripe, a subscription can be canceled immediately, skipping the remaining period. In this case, all events are created simultaneously: - **Subscription renewal cancelled** - **Subscription expired (churned)** - **Access Level updated** to remove the user’s access If a refund is approved, a **Subscription refunded** event is also triggered when it’s approved. ### Subscription Reactivation Flow If a user cancels a subscription, it expires, and they later repurchase the same subscription, a **Subscription renewed** event will be created. Even if there’s a gap in access, Adapty treats this as a single transaction chain, linked by the `vendor_original_transaction_id`. So, the repurchase is considered a renewal. The **Access level updated** events will be created twice: - at the subscription end to revoke the user's access - at the subscription repurchase to grant access ### Subscription Pause Flow (Android only) This flow applies when a user pauses and later resumes a subscription on Android. Pausing a subscription is not an immediate action. Even if the user pauses it during an active subscription period, the subscription will remain active, and the user will retain access until the end of that period. The subscription is officially paused at the end of the subscription period. At this point, the user loses access and remains without it until they choose to resume the subscription. No events are triggered when the user pauses the subscription. However, the following events are created at the end of the subscription period: - **Subscription paused (Android only)** - **Access level updated** to revoke the user's access When the user resumes the subscription, the following events are triggered: - **Subscription renewed** - **Access level updated** to restore the user's access These subscriptions will belong to the same transaction chain, linked with the same **vendor_original_transaction_id**. ## Trial Flows If you use trials in your app, you’ll receive additional trial-related events. ### Trial with Successful Conversion Flow The most common flow occurs when a user starts a trial, provides a credit card, and successfully converts to a standard subscription at the end of the trial period. In this situation, the following events are created at the moment of teh trial start: - **Trial started** to mark the trial start - **Access level updated** to grant access The **Trial converted** event is created when the standard subscription starts. ### Trial without Successful Conversion Flow If a user cancels the trial before it converts to a subscription, the following events are created at the time of cancellation: - **Trial renewal cancelled** to disable automatic conversion of the trial to a subscription - **Access level updated** to disable access renewal The user will have access until the end of the trial when the **Trial expired** event is created to mark the trial's end. ### Subscription Reactivation after Expired Trial Flow If a trial expires (due to a billing issue or cancellation) and the user later buys a subscription, the following events are created: - **Access level updated** to grant access to the user - **Trial converted** Even with a gap between the trial and subscription, Adapty links the two using `vendor_original_transaction_id`. This conversion is treated as part of a continuous transaction chain, starting with a zero-price trial. That is why the **Trial converted** event is created rather than the **Subscription started**. ## Product Changes This section covers any adjustments made to active subscriptions, such as upgrades, downgrades or purchases of a product from another group. ### Immediate Product Change Flow After a user changes a product, it can be changed in the system immediately before the subscription ends (mostly in case of an upgrade or replacement of a product). In this case, at the moment of the product change: - The access level is changed, and two **Access level updated** events are created: 1. to remove the access to the first product. 2. to give access to the second product. - The old subscription ends, and a refund is paid (the **Subscription refunded** event is created with the `cancellation_reason` = `upgraded`). Please note that no **Subscription expired (churned)** event is created; the **Subscription refunded** event replaces it. - The new subscription starts (the **Subscription started** event is created for the new product). If a user downgrades the subscription, the first subscription will last till the end of the paid period, and when the subscription ends, it will be replaced with a new, lower-tier subscription. In this situation, only the **Access level updated** event to disable access autorenewal will be created at once. All other events will be created at the moment of the subscription's actual replacement: - Another **Access level updated** event is created to give access to the second product. - The **Subscription expired (churned)** event is created to end the subscription for the first product. - The **Subscription started** event is created to start a new subscription for the new product. ### Delayed Product Change Flow There is also a variant when a user changes the product at the moment of the subscription renewal. This variant is very similar to the previous one: one **Access level updated** event will be created at once to disable access autorenewal for the old product. All other events will be created at the moment when the user changes the subscription and it is changed in the system: - Another **Access level updated** event is created to give access to the second product. - The **Subscription expired (churned)** event is created to end the subscription for the first product. - The **Subscription started** event is created to start a new subscription for the new product. ## Billing Issue Outcome Flow If attempts to convert a trial or renew a subscription fail due to a billing issue, what happens next depends on whether a grace period is enabled. With a grace period, if the payment succeeds, the trial converts or the subscription renews. If it fails, the app store will continue to attemp to charge the user for the subscription and if still fails, the app store will end the trial or subscription itself. Therefore, at the moment of the billing issue, the following events are created in Adapty: - **Billing issue detected** - **Entered grace period** (if the grace period is enabled) - **Access level updated** to provide the access till the end of the grace period If the payment succeeds later, Adapty records a **Trial converted** or **Subscription renewed** event, and the user does not lose access. If the payment ultimately fails and the app store cancels the subscription, Adapty generates these events: - **Trial expired** or **Subscription expired (churned)** with `cancellation_reason: billing_error` - **Access level updated** to revoke the user's access Without a grace period, the Billing Retry Period (the period when the app store continues to attempt to charge the user) starts immediately. If the payment never succeeds till the end of the grace period, the flow is the same: the same events are created when the app store ends the subscription automatically: - **Trial expired** or **Subscription expired (churned)** event with a `cancellation_reason` of `billing_error` - **Access level updated** to revoke the user's access ## Sharing Purchases Across User Accounts Flows When a [iOS](identifying-users#setting-customer-user-id-on-configuration), [Android](android-identifying-users#setting-customer-user-id-on-configuration), [Flutter](flutter-identifying-users#setting-customer-user-id-on-configuration), [React Native](react-native-identifying-users#setting-customer-user-id-on-configuration), and [Unity](unity-identifying-users#setting-customer-user-id-on-configuration) attempts to restore or extend a subscription already tied to a different [iOS](identifying-users#setting-customer-user-id-on-configuration), [Android](android-identifying-users#setting-customer-user-id-on-configuration), [Flutter](flutter-identifying-users#setting-customer-user-id-on-configuration), [React Native](react-native-identifying-users#setting-customer-user-id-on-configuration), and [Unity](unity-identifying-users#setting-customer-user-id-on-configuration), Adapty's **Sharing paid access between user accounts** setting controls how access is managed. The flow will vary depending on the selected option. ### Transfer Access to New User Flow The recommended option is to transfer the access level to the new user. This preserves the original user’s transaction history for consistent analytics. Only 2 **Access level updated** events will be created: 1. to remove the first user's access 2. to give access to the second user Here’s a breakdown of the fields related to access level assignment and transferring in the events generated in this scenario: - **User A: Access level updated (sent when User A purchases a subscription in the app)** ```json showLineNumbers { "profile_id": "00000000-0000-0000-0000-000000000000", "customer_user_id": UserA, "event_properties": { "profile_has_access_level": true, }, "profiles_sharing_access_level": null } ``` - **User A: Access level updated (sent when the app is reinstalled and User B logs in, revoking User A's access)** ```json showLineNumbers { "profile_id": "00000000-0000-0000-0000-000000000000", "customer_user_id": UserA, "event_properties": { "profile_has_access_level": false, }, "profiles_sharing_access_level": null } ``` - **User B: Access level updated (sent when User B logs in and access is granted)** ```json showLineNumbers { "profile_id": "00000000-0000-0000-0000-000000000001", "customer_user_id": UserB, "event_properties": { "profile_has_access_level": true, }, "profiles_sharing_access_level": null } ``` ### Shared Access Between Users Flow This option allows multiple users to share the same access level if their device is signed in to the same Apple/Google ID. This is useful when a user reinstalls the app and logs in with a different email — they'll still have access to their previous purchase. With this option, multiple identified users can share the same access level. While the access level is shared, all transactions are logged under the original [iOS](identifying-users#setting-customer-user-id-on-configuration), [Android](android-identifying-users#setting-customer-user-id-on-configuration), [Flutter](flutter-identifying-users#setting-customer-user-id-on-configuration), [React Native](react-native-identifying-users#setting-customer-user-id-on-configuration), and [Unity](unity-identifying-users#setting-customer-user-id-on-configuration) to maintain complete transaction history and analytics. Therefore, only 1 event will be created: **Access level updated** to grant access to the second user. Here’s a breakdown of the fields related to access level assignment and sharing in the events generated in this scenario: **User B: Access level updated (sent when User B logs in and access is granted)** ```json showLineNumbers { "profile_id": "00000000-0000-0000-0000-000000000000", "customer_user_id": UserA, "event_properties": { "profile_has_access_level": true, }, "profiles_sharing_access_level": [ { "profile_id": "00000000-0000-0000-0000-000000000001, "customer_user_id": UserB } ] } ``` ### Access Not Shared Between Users Flow With this option, only the first user profile to receive the access level retains it permanently. This is ideal if purchases need to be tied to a single [iOS](identifying-users#setting-customer-user-id-on-configuration), [Android](android-identifying-users#setting-customer-user-id-on-configuration), [Flutter](flutter-identifying-users#setting-customer-user-id-on-configuration), [React Native](react-native-identifying-users#setting-customer-user-id-on-configuration), and [Unity](unity-identifying-users#setting-customer-user-id-on-configuration). --- # File: event-statuses.md --- --- title: "Integration event statuses" description: "" --- Adapty determines deliverability based on the HTTP status code, considering any response outside the `200-399` range as a failure. You can track the status of integration events in the **Event List** within the Adapty Dashboard. The system displays statuses for all enabled integrations, regardless of whether a specific event type is turned on for a given integration. - Black: The event was successfully sent. - Grey: The event type is disabled for this integration. - Red: There is an issue with the integration that requires attention. For more details on failed events, hover over the integration name to see a tooltip with specific error information. The **Event Feed** displays data from the past two weeks to optimize performance. This limitation improves page loading speed, making it easier for users to navigate and analyze events efficiently. --- # File: events.md --- --- title: "Events to send to 3d-party integrations" description: "Track key subscription events using Adapty's analytics tools." --- Apple and Google send subscription events directly to servers via [App Store Server Notifications](enable-app-store-server-notifications) and [Real-time Developer Notifications (RTDN)](enable-real-time-developer-notifications-rtdn). As a result, mobile apps cannot reliably send events to analytics systems in real time. For example, if a user subscribes but never reopens the app, the developer won't receive any subscription status updates without a server. Adapty bridges this gap by collecting subscription data and converting it into human-readable events. These integration events are sent in JSON format. While all events share the same structure, their fields vary depending on the event type, store, and specific configuration. You can find the exact fields included in each event on the respective integration pages. To understand how to determine whether an event was successfully processed or if something went wrong, check the [Event statuses](event-statuses.md) page. ## Event types Most events are created and sent to all configured integrations if they’re enabled. However, the **Access level updated** event only triggers if the [webhook integration](webhook) is configured and this event is enabled. This event will appear in the [Event Feed](https://app.adapty.io/event-feed) and will also be sent to the webhook, but it won’t be shared with other integrations. If a webhook integration isn’t configured or this event type isn’t enabled, the **Access level updated** event won’t be created and won’t appear in the [Event Feed](https://app.adapty.io/event-feed). | Event name | Description | |:-----------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | subscription_started | Triggered when a user activates a paid subscription without a trial period, meaning they are billed instantly. | | subscription_renewed | Occurs when a subscription is renewed and the user is charged. This event starts from the second billing, whether it's a trial or non-trial subscription. | | subscription_renewal_cancelled | A user has turned off subscription auto-renewal. The user retains access to premium features until the end of the paid subscription period. | | subscription_renewal_reactivated | Triggered when a user reactivates subscription auto-renewal. | | subscription_expired | Triggered when a subscription fully ends after being canceled. For instance, if a user cancels a subscription on December 12th but it remains active until December 31st, the event is recorded on December 31st when the subscription expires. | | subscription_paused | Occurs when a user activates [subscription pause](https://developer.android.com/google/play/billing/subs#pause) (Android only). | | subscription_deferred | Triggered when a subscription purchase is [deferred](https://adapty.io/glossary/subscription-purchase-deferral/), allowing users to delay payment while maintaining access to premium features. This feature is available through the Google Play Developer APIand can be used for free trials or to accommodate users facing financial challenges. | | non_subscription_purchase | Any non-subscription purchase, such as lifetime access or consumable products like in-game coins. | | trial_started | Triggered when a user activates a trial subscription. | | trial_converted | Occurs when a trial ends and the user is billed (first purchase). For example, if a user has a trial until January 14th but is billed on January 7th, this event is recorded on January 7th. | | trial_renewal_cancelled | A user turned off subscription auto-renewal during the trial period. The user retains access to premium features until the trial ends but will not be billed or start a subscription. | | trial_renewal_reactivated | Occurs when a user reactivates subscription auto-renewal during the trial period. | | trial_expired | Triggered when a trial ends without converting to a subscription. | | entered_grace_period | Occurs when a payment attempt fails, and the user enters a grace period (if enabled). The user retains premium access during this time. | | billing_issue_detected | Triggered when a billing issue occurs during a charge attempt (e.g., insufficient card balance). | | subscription_refunded | Triggered when a subscription is refunded (e.g., by Apple Support). | | non_subscription_purchase_refunded | Triggered when a non-subscription purchase is refunded. | | access_level_updated | Occurs when a user's access level is updated. | The events above fully cover the users' state in terms of purchases. Let's look at some examples. ### Example 1 _The user activated a monthly subscription on April 1st with a 7-day trial. On the 4th day, he unsubscribed._ In that case, the following events will be sent: 1. `trial_started` on April 1st 2. `trial_renewal_cancelled` on 4th April 3. `trial_expired` on 7th April ### Example 2 _The user activated a monthly subscription on April 1st with a 7-day trial. On the 10th day, he unsubscribed._ In that case, the following events will be sent: 1. `trial_started` on April 1st 2. `trial_converted` on April 7th 3. `subscription_renewal_cancelled` on April 10th 4. `subscription_expired` on May 1st For a detailed breakdown of which events are triggered in each scenario, check out the [Event flows](event-flows.md). --- # File: expired-churned-trials.md --- --- title: "Expired (churned) trials" description: "Manage expired and churned trials effectively with Adapty analytics." --- The Expired (churned) trials chart displays the number of trials that have expired, leaving users without access to the app's premium features. In most cases, this occurs when users decide not to pay for the app or experience billing issues. ### Calculation Adapty calculates the number of "Expired (Churned) Trials" during a specific period by counting the number of trials that have ended and users no longer have access to premium app features. This count remains unchanged regardless of the reason for the trial's expiration, such as a user's decision not to pay for the app or billing issues. In addition, Adapty allows you to group the chart by expiration reason, such as user-initiated cancellations or billing issues. This grouping provides valuable insights into why users are churning and enables you to take proactive measures to prevent churn and optimize your app's success. ### Available filters and grouping - ✅ Filter by: Attribution, country, paywall, store, product, and duration. - ✅ Group by: Expiration reason, product, country, store, paywall, duration, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative. You can find more information about the available controls, filters, grouping options, and how to use them in the[ this documentation.](controls-filters-grouping-compare-proceeds) ### Expired (churned) trials chart usage The Expired (churned) trials chart is a valuable tool that provides insights into the number of trial periods that have ended, leaving users without access to premium app features. By monitoring the number of users who have churned during a specific period, you can identify patterns in user behavior and billing issues, and take proactive measures to reduce churn and improve user retention. By leveraging the insights provided by this chart, you can adjust your app's strategy and billing practices to encourage users to continue their subscriptions. ### Similar metrics In addition to the Trials renewal canceled chart, Adapty also provides metrics for other trial-related events, such as New Trials, Active Trials, and Expired Trials. To learn more about these trial-related metrics, please refer to the following documentation: - [New trials](new-trials) - [Active trials](active-trials) - [Trials renewal canceled](trials-renewal-cancelled) --- # File: export-analytics-api-authorization.md --- --- title: Authorization and request format for Exporting analytics API toc_max_heading_level: 2 --- ## 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](export-analytics-api-retrieve-analytics-data) - [Retrieve cohort data](export-analytics-api-retrieve-cohort-data) - [Retrieve conversion data](export-analytics-api-retrieve-conversion-data) - [Retrieve funnel data](export-analytics-api-retrieve-funnel-data) - [Retrieve Lifetime Value (LTV) data](export-analytics-api-retrieve-ltv) - [Retrieve retention data](export-analytics-api-retrieve-retention-data) --- # File: export-analytics-api-requests.md --- --- title: Exporting analytics API requests toc_max_heading_level: 2 --- 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](export-analytics-api-retrieve-analytics-data) - [Retrieve cohort data](export-analytics-api-retrieve-cohort-data) - [Retrieve conversion data](export-analytics-api-retrieve-conversion-data) - [Retrieve funnel data](export-analytics-api-retrieve-funnel-data) - [Retrieve Lifetime Value (LTV) data](export-analytics-api-retrieve-ltv) - [Retrieve retention data](export-analytics-api-retrieve-retention-data) --- # File: export-analytics-api-retrieve-analytics-data.md --- --- title: Retrieve analytics data toc_max_heading_level: 2 --- Retrieves analytics data for insights on user behavior and performance metrics to further use in charts. ## Endpoint and method ```http POST https://api-admin.adapty.io/api/v1/client-api/metrics/analytics/ ``` ## Request example ```bash curl --location 'https://api-admin.adapty.io/api/v1/client-api/metrics/analytics/' \ --header 'Authorization: Api-Key ' \ --header 'Content-Type: application/json' \ --data '{ "chart_id": "revenue", "filters": { "date": [ "2024-01-01", "2024-12-31" ], "country": [ "us" ], "attribution_channel": [ "social_media_influencers" ] }, "period_unit": "week", "segmentation": "attribution_campaign" }' ``` ```python showLineNumbers url = "https://api-admin.adapty.io/api/v1/client-api/metrics/analytics/" payload = json.dumps({ "chart_id": "revenue", "filters": { "date": [ "2024-01-01", "2024-12-31" ], "country": [ "us" ], "attribution_channel": [ "social_media_influencers" ] }, "period_unit": "week", "segmentation": "attribution_campaign" }) headers = 'Authorization': "Api-Key ", 'Content-Type': "application/json"} response = requests.request("POST", url, headers=headers, data=payload) print(response.text) ``` ```javascript showLineNumbers const myHeaders = new Headers(); myHeaders.append("Authorization", "Api-Key "); myHeaders.append("Content-Type", "application/json"); const raw = JSON.stringify({ "chart_id": "revenue", "filters": { "date": [ "2024-01-01", "2024-12-31" ], "country": [ "us" ], "attribution_channel": [ "social_media_influencers" ] }, "period_unit": "week", "segmentation": "attribution_campaign" }); const requestOptions = { method: "POST", headers: myHeaders, body: raw, redirect: "follow" }; fetch("https://api-admin.adapty.io/api/v1/client-api/metrics/analytics/", requestOptions) .then((response) => response.text()) .then((result) => console.log(result)) .catch((error) => console.error(error)); ``` ## Parameters | Name | Type | Required | Description. | |--------------|-------------------------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | filters | [MetricsFilters](#metricsfilters-object) object | Yes | An object containing filter parameters. See details below this table. | | chart_id | String | Yes |

Specify which chart you need. You can specify only one chart type in each request. See more on the [analytics charts](analytics-charts).

Possible values are:

  • revenue
  • mrr
  • arr
  • arppu
  • subscriptions_active
  • subscriptions_new
  • subscriptions_renewal_cancelled
  • subscriptions_expired
  • trials_active
  • trials_new
  • trials_renewal_cancelled
  • trials_expired
  • grace_period
  • billing_issue
  • refund_events
  • refund_money
  • non_subscriptions
  • arpu
  • installs
| | period_unit | String | No | Specify the time interval for aggregating analytics data, so you can view results grouped by days, weeks, months, etc. Possible values are:
  • day
  • week
  • month (default)
  • quarter
  • year
| | date_type | String | No | Possible values are:
  • purchase_date (default)
  • profile_install_date
| | segmentation | String | No | Sets the basis for segmentation. See which segmentation is available for different charts in the [Segmentation](#segmentation) table below. | | format | String | No | Specify the export file format. Available options are:
  • json (default)
  • csv
| ### Segmentation Different charts can use different types of segmentation: ### For ARPU - country - store - attribution_status - attribution_channel - attribution_campaign - attribution_adgroup - attribution_adset - attribution_creative - attribution_source - period ### For revenue, MRR, ARR, active subscriptions, and active trials - country - store - attribution_status - attribution_channel - attribution_campaign - attribution_adgroup - attribution_adset - attribution_creative - attribution_source - store_product_id - paywall_id - audience_id - placement_id - duration - renewal_status - period - offer_category - offer_type - offer_id ### For ARPPU - country - store - attribution_status - attribution_channel - attribution_campaign - attribution_adgroup - attribution_adset - attribution_creative - attribution_source - store_product_id - paywall_id - audience_id - placement_id - duration - renewal_status - period ### For new subscriptions, new trials, and refund events - country - store - attribution_status - attribution_channel - attribution_campaign - attribution_adgroup - attribution_adset - attribution_creative - attribution_source - store_product_id - paywall_id - audience_id - placement_id - duration - offer_category - offer_type - offer_id ### For expired subscriptions and expired trials - country - store - attribution_status - attribution_channel - attribution_campaign - attribution_adgroup - attribution_adset - attribution_creative - attribution_source - store_product_id - paywall_id - audience_id - placement_id - duration - cancellation_reason ### For cancelled subscription renewals, cancelled trials, grace periods, billing issues, money refunds, and non-subscription purchases - country - store - attribution_status - attribution_channel - attribution_campaign - attribution_adgroup - attribution_adset - attribution_creative - attribution_source - store_product_id - paywall_id - audience_id - placement_id - duration ### For installs - country - store - attribution_status - attribution_channel - attribution_campaign - attribution_adgroup - attribution_adset - attribution_creative - attribution_source ### MetricsFilters object Filtration criteria differ for different charts. See the variants below: ### For ARPU and installs | Name | Type | Required | Description. | | -------------------- | ---------------------------- | ------------------ | ------------------------------------------------------------ | | date | array of String values | :heavy_plus_sign: | Enter the date or time period for which you want to retrieve chart data. | | store | array of String values | :heavy_minus_sign: | Filter by the app store where the purchase was made. Possible values include **app_store**, **play_store**, **stripe**, and any custom store ID. If you use a custom store, enter its ID as set in the Adapty Dashboard. | | country | array of String values | :heavy_minus_sign: | Filter by the 2-letter country code where the purchase took place, using ISO 3166-1 standard codes. | | attribution_source | array of String values | :heavy_minus_sign: | The source integration for attribution. Possible options:
  • adjust
  • airbridge
  • apple_search_ads
  • appsflyer
  • branch
  • custom
| | attribution_status | array of String values | :heavy_minus_sign: | Indicates if the attribution is organic or non-organic. Possible values:
  • organic
  • non-organic
  • unknown
| | attribution_channel | array of String values | :heavy_minus_sign: | Marketing channel that led to the transaction. | | attribution_campaign | array of String values | :heavy_minus_sign: | Marketing campaign that brought the transaction. | | attribution_adgroup | array of String values | :heavy_minus_sign: | Attribution ad group that brought the transaction. | | attribution_adset | array of String values | :heavy_minus_sign: | Attribution ad set that led to the transaction. | | attribution_creative | array of String values | :heavy_minus_sign: | Specific visual or text elements in an ad or campaign tracked to measure effectiveness (e.g., clicks, conversions). | ### For cancelled trials, expired trials, grace periods, billing issues, cancelled subscription renewals, and expired subscriptions | Name | Type | Required | Description. | | -------------------- | ---------------------------- | ------------------ | ------------------------------------------------------------ | | date | array of String values | :heavy_plus_sign: | Enter the date or period for which you want to retrieve chart data. | | store | array of String values | :heavy_minus_sign: | Filter by the app store where the purchase was made. Possible values include **app_store**, **play_store**, **stripe**, and any custom store ID. If you use a custom store, enter its ID as set in the Adapty Dashboard. | | country | array of String values | :heavy_minus_sign: | Filter by the 2-letter country code where the purchase took place, using ISO 3166-1 standard codes. | | store_product_id | array of String values | :heavy_minus_sign: | Unique identifier of a product from the app store. You can find this ID in the [Products](https://app.adapty.io/products) section of the Adapty Dashboard. | | duration | array of String | :heavy_minus_sign: | Specify the subscription duration. Possible values:
  • Weekly
  • Monthly
  • 2 months
  • 3 months
  • 6 months
  • Annual
  • Lifetime
  • Uncategorized
| | attribution_source | array of String values | :heavy_minus_sign: | The source integration for attribution. Possible options:
  • adjust
  • airbridge
  • apple_search_ads
  • appsflyer
  • branch
  • custom
| | attribution_status | array of String values | :heavy_minus_sign: | Indicates if the attribution is organic or non-organic. Possible values:
  • organic
  • non-organic
  • unknown
| | attribution_channel | array of String values | :heavy_minus_sign: | Marketing channel that led to the transaction. | | attribution_campaign | array of String values | :heavy_minus_sign: | Marketing campaign that brought the transaction. | | attribution_adgroup | array of String values | :heavy_minus_sign: | Attribution ad group that brought the transaction. | | attribution_adset | array of String values | :heavy_minus_sign: | Attribution ad set that led to the transaction. | | attribution_creative | array of String values | :heavy_minus_sign: | Specific visual or text elements in an ad or campaign tracked to measure effectiveness (e.g., clicks, conversions). | ### For all other charts | Name | Type | Required | Description | | ------------------------ | ---------------------------- | ------------------ | ------------------------------------------------------------ | | date | array of String values(data) | :heavy_plus_sign: | Enter the date or period for which you want to retrieve chart data. | | compare_date | array of String values(data) | :heavy_minus_sign: | If comparing two dates or periods, enter the earlier one here and the later one in the `date` parameter. | | store | array of String values | :heavy_minus_sign: | Filter by the app store where the purchase was made. Possible values include **app_store**, **play_store**, **stripe**, and any custom store ID. If using a custom store, enter its ID as set in the Adapty Dashboard. | | country | array of String values | :heavy_minus_sign: | Filter by the 2-letter country code where the purchase took place, using ISO 3166-1 standard codes. | | store_product_id | array of String values | :heavy_minus_sign: | Unique identifier of a product from the app store. You can see this ID in the [**Products**](https://app.adapty.io/products) section of the Adapty Dashboard. | | duration | array of String | :heavy_minus_sign: | Specify the subscription duration. Possible values are:
  • Weekly
  • Monthly
  • 2 months
  • 3 months
  • 6 months
  • Annual
  • Lifetime
  • Uncategorized
| | attribution_source | array of String values | :heavy_minus_sign: | The source integration for attribution. Possible options:
  • adjust
  • airbridge
  • apple_search_ads
  • appsflyer
  • branch
  • custom
| | attribution_status | array of String values | :heavy_minus_sign: | Indicates if the attribution is organic or non-organic. Possible values are:
  • organic
  • non-organic
  • unknown
| | attribution_channel | array of String values | :heavy_minus_sign: | Marketing channel that led to the transaction. | | attribution_campaign | array of String values | :heavy_minus_sign: | Marketing campaign that brought the transaction. | | attribution_adgroup | array of String values | :heavy_minus_sign: | Attribution ad group that brought the transaction. | | attribution_adset | array of String values | :heavy_minus_sign: | Attribution ad set that led to the transaction. | | attribution_creative | array of String values | :heavy_minus_sign: | Specific visual or text elements in an ad or campaign tracked to measure effectiveness (e.g., clicks, conversions). | | offer_category | array of String values | :heavy_minus_sign: | Specify the offer categories you want to retrieve data for. Possible values are:
  • introductory
  • promotional
  • winback
| | offer_type | array of String values | :heavy_minus_sign: | Specify the offer types you want to retrieve data for. Possible values are:
  • free_trial
  • pay_as_you_go
  • pay_up_front
| | offer_id | array of String values | :heavy_minus_sign: | Specify the specific offers you want to retrieve data for. | --- # File: export-analytics-api-retrieve-cohort-data.md --- --- title: Retrieve cohort data toc_max_heading_level: 2 --- Retrieve cohort data to track user groups over time. ## Endpoint and method ```http POST https://api-admin.adapty.io/api/v1/client-api/metrics/cohort/ ``` ## Request example ```bash curl --location 'https://api-admin.adapty.io/api/v1/client-api/metrics/cohort/' \ --header 'Authorization: Api-Key ' \ --header 'Content-Type: application/json' \ --data '{ "filters": { "date": [ "2024-04-01", "2024-09-30" ], "store": [ "app_store" ], "country": [ "us" ] }, "period_unit": "month", "period_type": "renewals", "value_type": "absolute", "value_field": "subscriptions" }' ``` ```python showLineNumbers url = "https://api-admin.adapty.io/api/v1/client-api/metrics/cohort/" payload = json.dumps({ "filters": { "date": [ "2024-04-01", "2024-09-30" ], "store": [ "app_store" ], "country": [ "us" ] }, "period_unit": "month", "period_type": "renewals", "value_type": "absolute", "value_field": "subscriptions" }) headers = { 'Authorization': "Api-Key ", 'Content-Type': "application/json" } response = requests.request("POST", url, headers=headers, data=payload) print(response.text) ``` ```javascript showLineNumbers const myHeaders = new Headers(); myHeaders.append("Authorization", "Api-Key "); myHeaders.append("Content-Type", "application/json"); const raw = JSON.stringify({ "filters": { "date": [ "2024-04-01", "2024-09-30" ], "store": [ "app_store" ], "country": [ "us" ] }, "period_unit": "month", "period_type": "renewals", "value_type": "absolute", "value_field": "subscriptions" }); const requestOptions = { method: "POST", headers: myHeaders, body: raw, redirect: "follow" }; fetch("https://api-admin.adapty.io/api/v1/client-api/metrics/cohort/", requestOptions) .then((response) => response.text()) .then((result) => console.log(result)) .catch((error) => console.error(error)); ``` ## Parameters | Name | Type | Required | Description | | ----------------- | ------------------------------------------------------------ | ------------------ | ------------------------------------------------------------ | | filters | [MetricsFilters](https://adapty.io/docs/controls-filters-grouping-compare-proceeds#filtering-and-grouping) object | :heavy_plus_sign: | An object containing filtration parameters. See details below this table. | | period_unit | String | :heavy_minus_sign: | Specify the time interval for aggregating analytics data to view results grouped by selected periods (days, weeks, months, etc.). Possible values are:
  • day
  • week
  • month (default)
  • quarter
  • year
| | period_type | String | :heavy_minus_sign: | Analyze data by renewals or by days. For a detailed description, see [Tracking by renewals or by days](https://adapty.io/docs/analytics-cohorts#cohorts-by-renewals-or-by-days). Possible values are:
  • renewals (default)
  • days
| | value_type | String | :heavy_minus_sign: | Specify how values are displayed. Possible values are:
  • absolute (default): as a percentage of the total
  • relative: as a percentage from the start, starting at 100% for renewal periods
| | value_field | String | :heavy_minus_sign: | Specify the type of values displayed. Possible values are:
  • revenue (default)
  • arppu
  • arpu
  • arpas
  • subscribers
  • subscriptions
| | accounting_type | String | :heavy_minus_sign: | The accounting method used. Possible values are:
  • revenue (default)
  • proceeds
  • net_revenue
| | renewal_days | Array of Integers | :heavy_minus_sign: | This is a list of days since the app was installed for the cohort type `period_type=days`. Default: 0, 3, 7, 14, 28, 31, 61, 92, 183, 336, 550, 731. | | prediction_months | Integer | :heavy_minus_sign: | Enter how many months of prediction you want. Possible values: 3, 6, 9, 12 (default), 18, 24. | | format | String | :heavy_minus_sign: | Specify the export file format. Available options are:
  • json (default)
  • csv
| ### MetricsFilters object | Name | Type | Required | Description | | -------------------- | ---------------------------- | ------------------ | ------------------------------------------------------------ | | date | array of String values(data) | :heavy_plus_sign: | Enter the date or period for which you want to retrieve chart data. | | store | array of String values | :heavy_minus_sign: | Filter by the app store where the purchase was made. Possible values include app_store, play_store, stripe, and any custom store ID. If using a custom store, enter its ID as set in the Adapty Dashboard. | | country | array of String values | :heavy_minus_sign: | Filter by the 2-letter country code where the purchase took place, using ISO 3166-1 standard codes. | | store_product_id | array of String values | :heavy_minus_sign: | Unique identifier of a product from the app store. You can see this ID in the [Products](https://app.adapty.io/products) section of the Adapty Dashboard. | | duration | array of String | :heavy_minus_sign: | Specify the subscription duration. Possible values are:
  • Weekly
  • Monthly
  • 2 months
  • 3 months
  • 6 months
  • Annual
  • Lifetime
  • Uncategorized
| | attribution_source | array of String values | :heavy_minus_sign: | The source integration for attribution. Possible options:
  • adjust
  • airbridge
  • apple_search_ads
  • appsflyer
  • branch
  • custom
| | attribution_status | array of String values | :heavy_minus_sign: | Indicates if the attribution is organic or non-organic. Possible values are:
  • organic
  • non-organic
  • unknown
| | attribution_channel | array of String values | :heavy_minus_sign: | Marketing channel that led to the transaction. | | attribution_campaign | array of String values | :heavy_minus_sign: | Marketing campaign that brought the transaction. | | attribution_adgroup | array of String values | :heavy_minus_sign: | Attribution ad group that brought the transaction. | | attribution_adset | array of String values | :heavy_minus_sign: | Attribution ad set that led to the transaction. | | attribution_creative | array of String values | :heavy_minus_sign: | Specific visual or text elements in an ad or campaign tracked to measure effectiveness (e.g., clicks, conversions). | | offer_category | array of String values | :heavy_minus_sign: | Specify the offer categories you want to retrieve data for. Possible values are:
  • introductory
  • promotional
  • winback
| | offer_type | array of String values | :heavy_minus_sign: | Specify the offer types you want to retrieve data for. Possible values are:
  • free_trial
  • pay_as_you_go
  • pay_up_front
| | offer_id | array of String values | :heavy_minus_sign: | Specify the specific offers you want to retrieve data for. | --- # File: export-analytics-api-retrieve-conversion-data.md --- --- title: Retrieve conversion data toc_max_heading_level: 2 --- Retrieves conversion data to analyze user actions and measure the effectiveness of marketing efforts over time. ## Endpoint and method ```http POST https://api-admin.adapty.io/api/v1/client-api/metrics/conversion/ ``` ## Request example ```bash curl --location 'https://api-admin.adapty.io/api/v1/client-api/metrics/conversion/' \ --header 'Authorization: Api-Key ' \ --header 'Content-Type: application/json' \ --data '{ "filters": { "date": [ "2024-01-01", "2024-12-31" ] }, "from_period": 1, "to_period": "6+", "period_unit": "month", "date_type": "purchase_date", "segmentation": "country", "format": "csv" }' ``` ```python showLineNumbers url = "https://api-admin.adapty.io/api/v1/client-api/metrics/conversion/" payload = json.dumps({ "filters": { "date": [ "2024-01-01", "2024-12-31" ] }, "from_period": 1, "to_period": "6+", "period_unit": "month", "date_type": "purchase_date", "segmentation": "country", "format": "csv" }) headers = { 'Authorization': "Api-Key ", 'Content-Type': "application/json" } response = requests.request("POST", url, headers=headers, data=payload) print(response.text) ``` ```javascript showLineNumbers const myHeaders = new Headers(); myHeaders.append("Authorization", "Api-Key "); myHeaders.append("Content-Type", "application/json"); const raw = JSON.stringify({ "filters": { "date": [ "2024-01-01", "2024-12-31" ] }, "from_period": 1, "to_period": "6+", "period_unit": "month", "date_type": "purchase_date", "segmentation": "country", "format": "csv" }); const requestOptions = { method: "POST", headers: myHeaders, body: raw, redirect: "follow" }; fetch("https://api-admin.adapty.io/api/v1/client-api/metrics/conversion/", requestOptions) .then((response) => response.text()) .then((result) => console.log(result)) .catch((error) => console.error(error)); ``` ## Parameters | Name | Type | Required | Description. | | ------------ | ---------------------------------------- | ------------------ | ------------------------------------------------------------ | | filters | [MetricsFilters](#metricsfilters-object) | :heavy_plus_sign: | An object containing filtration parameters. See details below this table. | | from_period | String/null | :heavy_plus_sign: | The user’s starting subscription state of the conversation (e.g., `null` = install, `0` = trial, `1` = first paid period, etc.). See [Conversion types](#conversion-types) for valid values. | | to_period | String | :heavy_plus_sign: | The user’s new subscription state after the conversion (e.g., `0` = trial, `1` = first paid period, `6+` = after six months, etc.). See [Conversion types](#conversion-types) for valid values. | | period_unit | String | :heavy_minus_sign: |

Specify the time interval for aggregating analytics data, to view results grouped by selected periods, such as days, weeks, months, etc. Possible values are:

  • day
  • week
  • month (default)
  • quarter
  • year
| | date_type | String | :heavy_minus_sign: | Specify which date should be treated as a user joining date. Possible values:
  • purchase_date (default)
  • profile_install_date
| | segmentation | String | :heavy_minus_sign: |

Sets the basis for segmentation. Possible values are:

  • app_id
  • period
  • renewal_status
  • cancellation_reason
  • store_product_id
  • country
  • store
  • purchase_container_id
  • paywall_id
  • audience_id
  • placement_id
  • attribution_source
  • attribution_status
  • attribution_channel
  • attribution_campaign
  • attribution_adgroup
  • attribution_adset
  • attribution_creative
  • duration
  • default
| | format | String | :heavy_minus_sign: |

Specify the export file format. Available options are:

  • json
  • csv
| ### MetricsFilters object | Name | Type | Required | Description | | ------------------------ | ---------------------------- | ------------------ | ------------------------------------------------------------ | | date | array of String values(data) | :heavy_plus_sign: | Enter the date or period for which you want to retrieve chart data. | | compare_date | array of String values(data) | :heavy_minus_sign: | If comparing two dates or periods, enter the earlier one here and the later one in the `date` parameter. | | store | array of String values | :heavy_minus_sign: | Filter by the app store where the purchase was made. Possible values include **app_store**, **play_store**, **stripe**, and any custom store ID. If using a custom store, enter its ID as set in the Adapty Dashboard. | | country | array of String values | :heavy_minus_sign: | Filter by the 2-letter country code where the purchase took place, using ISO 3166-1 standard codes. | | store_product_id | array of String values | :heavy_minus_sign: | Unique identifier of a product from the app store. You can see this ID in the [**Products**](https://app.adapty.io/products) section of the Adapty Dashboard. | | duration | array of String | :heavy_minus_sign: | Specify the subscription duration. Possible values are:
  • Weekly
  • Monthly
  • 2 months
  • 3 months
  • 6 months
  • Annual
  • Lifetime
  • Uncategorized
| | attribution_source | array of String values | :heavy_minus_sign: | The source integration for attribution. Possible options:
  • adjust
  • airbridge
  • apple_search_ads
  • appsflyer
  • branch
  • custom
| | attribution_status | array of String values | :heavy_minus_sign: | Indicates if the attribution is organic or non-organic. Possible values are:
  • organic
  • non-organic
  • unknown
| | attribution_channel | array of String values | :heavy_minus_sign: | Marketing channel that led to the transaction. | | attribution_campaign | array of String values | :heavy_minus_sign: | Marketing campaign that brought the transaction. | | attribution_adgroup | array of String values | :heavy_minus_sign: | Attribution ad group that brought the transaction. | | attribution_adset | array of String values | :heavy_minus_sign: | Attribution ad set that led to the transaction. | | attribution_creative | array of String values | :heavy_minus_sign: | Specific visual or text elements in an ad or campaign tracked to measure effectiveness (e.g., clicks, conversions). | | offer_category | array of String values | :heavy_minus_sign: | Specify the offer categories you want to retrieve data for. Possible values are:
  • introductory
  • promotional
  • winback
| | offer_type | array of String values | :heavy_minus_sign: | Specify the offer types you want to retrieve data for. Possible values are:
  • free_trial
  • pay_as_you_go
  • pay_up_front
| | offer_id | array of String values | :heavy_minus_sign: | Specify the specific offers you want to retrieve data for. | ## Conversion types Use `from_period` and `to_period` together to specify the exact conversion you want to analyze. Only transitions supported by the Adapty Dashboard are available: | Conversation | from_period | to_period | | ------------------------------------------------------------ | ----------- | --------- | | **Install → Trial**

The user has just installed the app (no subscription yet) and started a free trial.

| null | 0 | | **Install → Paid**

The user has just installed the app and jumped straight to a paid subscription.

| null | 1 | | **Trial → Paid**

The user switched from a free trial to a paid subscription.

| 0 | 1 | | **Paid → 2nd period**

The user renewed from the first paid period to the second.

| 1 | 2 | | **2nd → 3rd period**

The user renewed from the second paid period to the third.

| 2 | 3 | | **3rd → 4th period**

The user renewed from the third paid period to the fourth.

| 3 | 4 | | **4th → 5th period**

The user renewed from the fourth paid period to the fifth.

| 4 | 5 | | **Paid → 6 months+**

The user stayed on a paid subscription for six months or longer.

| 1 | "6+" | | **Paid → 1 year+**

The user stayed on a paid subscription for a year or longer.

| 1 | "12+" | | **Paid → 2 years+**

The user stayed on a paid subscription for two years or longer.

| 1 | "24+" | --- # File: export-analytics-api-retrieve-funnel-data.md --- --- title: Retrieve funnel data toc_max_heading_level: 2 --- Retrieves funnel data to track user progression through specific stages of a conversion process. ## Endpoint and method ```http POST https://api-admin.adapty.io/api/v1/client-api/metrics/funnel/ ``` ## Request example ```bash curl --location 'https://api-admin.adapty.io/api/v1/client-api/metrics/funnel/' \ --header 'Authorization: Api-Key ' \ --header 'Content-Type: application/json' \ --data '{ "filters": { "date": [ "2024-01-01", "2024-12-31" ], "compare_date": [ "2023-01-01", "2023-12-31" ], "offer_category": [ "promotional" ] }, "period_unit": "quarter", "show_value_as": "absolute", "format": "csv" }' ``` ```python showLineNumbers url = "https://api-admin.adapty.io/api/v1/client-api/metrics/funnel/" payload = json.dumps({ "filters": { "date": [ "2024-01-01", "2024-12-31" ], "compare_date": [ "2023-01-01", "2023-12-31" ], "offer_category": [ "promotional" ] }, "period_unit": "quarter", "show_value_as": "absolute" "format": "csv" }) headers = { 'Authorization': "Api-Key ", 'Content-Type': "application/json" } response = requests.request("POST", url, headers=headers, data=payload) print(response.text) ``` ```javascript showLineNumbers const myHeaders = new Headers(); myHeaders.append("Authorization", "Api-Key "); myHeaders.append("Content-Type", "application/json"); const raw = JSON.stringify({ "filters": { "date": [ "2024-01-01", "2024-12-31" ], "compare_date": [ "2023-01-01", "2023-12-31" ], "offer_category": [ "promotional" ] }, "period_unit": "quarter", "show_value_as": "absolute", "format": "csv" }); const requestOptions = { method: "POST", headers: myHeaders, body: raw, redirect: "follow" }; fetch("https://api-admin.adapty.io/api/v1/client-api/metrics/funnel/", requestOptions) .then((response) => response.text()) .then((result) => console.log(result)) .catch((error) => console.error(error)); ``` ## Parameters | Name | Type | Required | Description | | ------------- | ---------------------------------------- | ------------------ | ------------------------------------------------------------ | | filters | [MetricsFilters](#metricsfilters-object) | :heavy_plus_sign: | An object containing filtration parameters. See details below this table. | | period_unit | String | :heavy_minus_sign: |

Specify the time interval for aggregating analytics data, to view results grouped by selected periods, such as days, weeks, months, etc. Possible values are:

  • day
  • week
  • month
  • quarter
  • year
| | show_value_as | String | :heavy_minus_sign: |

Specify how values are displayed. Possible values are:

  • absolute: as a percentage of the total
  • relative: as a percentage from the start, starting at 100% for renewal periods
  • both
| | segmentation | String | :heavy_minus_sign: |

Sets the basis for segmentation. Possible values are:

  • app_id
  • period
  • renewal_status
  • cancellation_reason
  • store_product_id
  • country
  • store
  • purchase_container_id
  • paywall_id
  • audience_id
  • placement_id
  • attribution_source
  • attribution_status
  • attribution_channel
  • attribution_campaign
  • attribution_adgroup
  • attribution_adset
  • attribution_creative
  • duration
  • default
| | format | String | :heavy_minus_sign: |

Specify the export file format. Available options are:

  • json
  • csv
| ### MetricsFilters object | Name | Type | Required | Description | | ------------------------ | ---------------------------- | ------------------ | ------------------------------------------------------------ | | date | array of String values(data) | :heavy_plus_sign: | Enter the date or period for which you want to retrieve chart data. | | compare_date | array of String values(data) | :heavy_minus_sign: | If comparing two dates or periods, enter the earlier one here and the later one in the `date` parameter. | | store | array of String values | :heavy_minus_sign: | Filter by the app store where the purchase was made. Possible values include **app_store**, **play_store**, **stripe**, and any custom store ID. If using a custom store, enter its ID as set in the Adapty Dashboard. | | country | array of String values | :heavy_minus_sign: | Filter by the 2-letter country code where the purchase took place, using ISO 3166-1 standard codes. | | store_product_id | array of String values | :heavy_minus_sign: | Unique identifier of a product from the app store. You can see this ID in the [**Products**](https://app.adapty.io/products) section of the Adapty Dashboard. | | duration | array of String | :heavy_minus_sign: | Specify the subscription duration. Possible values are:
  • Weekly
  • Monthly
  • 2 months
  • 3 months
  • 6 months
  • Annual
  • Lifetime
  • Uncategorized
| | attribution_source | array of String values | :heavy_minus_sign: | The source integration for attribution. Possible options:
  • adjust
  • airbridge
  • apple_search_ads
  • appsflyer
  • branch
  • custom
| | attribution_status | array of String values | :heavy_minus_sign: | Indicates if the attribution is organic or non-organic. Possible values are:
  • organic
  • non-organic
  • unknown
| | attribution_channel | array of String values | :heavy_minus_sign: | Marketing channel that led to the transaction. | | attribution_campaign | array of String values | :heavy_minus_sign: | Marketing campaign that brought the transaction. | | attribution_adgroup | array of String values | :heavy_minus_sign: | Attribution ad group that brought the transaction. | | attribution_adset | array of String values | :heavy_minus_sign: | Attribution ad set that led to the transaction. | | attribution_creative | array of String values | :heavy_minus_sign: | Specific visual or text elements in an ad or campaign tracked to measure effectiveness (e.g., clicks, conversions). | | offer_category | array of String values | :heavy_minus_sign: | Specify the offer categories you want to retrieve data for. Possible values are:
  • introductory
  • promotional
  • winback
| | offer_type | array of String values | :heavy_minus_sign: | Specify the offer types you want to retrieve data for. Possible values are:
  • free_trial
  • pay_as_you_go
  • pay_up_front
| | offer_id | array of String values | :heavy_minus_sign: | Specify the specific offers you want to retrieve data for. | --- # File: export-analytics-api-retrieve-ltv.md --- --- title: Retrieve Lifetime Value (LTV) data toc_max_heading_level: 2 --- Retrieves LTV data to assess the long-term revenue potential of customers over their engagement period. ## Endpoint and method ```http POST https://api-admin.adapty.io/api/v1/client-api/metrics/ltv/ ``` ## Request example ```bash curl --location 'https://api-admin.adapty.io/api/v1/client-api/metrics/ltv/' \ --header 'Authorization: Api-Key ' \ --header 'Content-Type: application/json' \ --data '{ "filters": { "date": [ "2024-01-01", "2024-12-31" ], "compare_date": [ "2023-01-01", "2023-12-31" ], "offer_category": [ "introductory" ], "store": [ "app_store" ], "country": [ "us" ], "attribution_source": [ "appsflyer" ], "attribution_status": [ "organic" ], "attribution_channel": [ "social_media" ] }, "period_unit": "quarter", "period_type": "renewals", "segmentation": "store_product_id", "format": "csv" }' ``` ```python showLineNumbers url = "https://api-admin.adapty.io/api/v1/client-api/metrics/ltv/" payload = json.dumps({ "filters": { "date": [ "2024-01-01", "2024-12-31" ], "compare_date": [ "2023-01-01", "2023-12-31" ], "offer_category": [ "introductory" ], "store": [ "app_store" ], "country": [ "us" ], "attribution_source": [ "appsflyer" ] }, "period_unit": "quarter", "period_type": "renewals", "segmentation": "store_product_id", "format": "csv" }) headers = { 'Authorization':"Api-Key ", 'Content-Type': "application/json" } response = requests.request("POST", url, headers=headers, data=payload) print(response.text) ``` ```javascript showLineNumbers const myHeaders = new Headers(); myHeaders.append("Authorization", "Api-Key "); myHeaders.append("Content-Type", "application/json"); const raw = JSON.stringify({ "filters": { "date": [ "2024-01-01", "2024-12-31" ], "compare_date": [ "2023-01-01", "2023-12-31" ], "offer_category": [ "introductory" ], "store": [ "app_store" ], "country": [ "us" ], "attribution_source": [ "appsflyer" ] }, "period_unit": "quarter", "period_type": "renewals", "segmentation": "store_product_id", "format": "csv" }); const requestOptions = { method: "POST", headers: myHeaders, body: raw, redirect: "follow" }; fetch("https://api-admin.adapty.io/api/v1/client-api/metrics/ltv/", requestOptions) .then((response) => response.text()) .then((result) => console.log(result)) .catch((error) => console.error(error)); ``` ## Parameters | Name | Type | Required | Description | | ------------ | ---------------------------------------- | ------------------ | ------------------------------------------------------------ | | filters | [MetricsFilters](#metricsfilters-object) | :heavy_plus_sign: | An object containing filtration parameters. See details below this table. | | period_unit | String | :heavy_minus_sign: |

Specify the time interval for aggregating analytics data, to view results grouped by selected periods, such as days, weeks, months, etc. Possible values are:

  • day
  • week
  • month
  • quarter
  • year
| | period_type | String | :heavy_minus_sign: |

Possible values are:

  • renewals
  • days
| | segmentation | String | :heavy_minus_sign: |

Possible values are:

  • day
  • week
  • month
  • year
  • country
  • product
  • paywall
  • paywalls_group
  • audience
  • placement
  • duration
  • store
| | format | String | :heavy_minus_sign: |

Specify the export file format. Available options are:

  • json
  • csv
| ### MetricsFilters object | Name | Type | Required | Description | | ------------------------ | ---------------------------- | ------------------ | ------------------------------------------------------------ | | date | array of String values(data) | :heavy_plus_sign: | Enter the date or period for which you want to retrieve chart data. | | compare_date | array of String values(data) | :heavy_minus_sign: | If comparing two dates or periods, enter the earlier one here and the later one in the `date` parameter. | | store | array of String values | :heavy_minus_sign: | Filter by the app store where the purchase was made. Possible values include **app_store**, **play_store**, **stripe**, and any custom store ID. If using a custom store, enter its ID as set in the Adapty Dashboard. | | country | array of String values | :heavy_minus_sign: | Filter by the 2-letter country code where the purchase took place, using ISO 3166-1 standard codes. | | store_product_id | array of String values | :heavy_minus_sign: | Unique identifier of a product from the app store. You can see this ID in the [**Products**](https://app.adapty.io/products) section of the Adapty Dashboard. | | duration | array of String | :heavy_minus_sign: | Specify the subscription duration. Possible values are:
  • Weekly
  • Monthly
  • 2 months
  • 3 months
  • 6 months
  • Annual
  • Lifetime
  • Uncategorized
| | attribution_source | array of String values | :heavy_minus_sign: | The source integration for attribution. Possible options:
  • adjust
  • airbridge
  • apple_search_ads
  • appsflyer
  • branch
  • custom
| | attribution_status | array of String values | :heavy_minus_sign: | Indicates if the attribution is organic or non-organic. Possible values are:
  • organic
  • non-organic
  • unknown
| | attribution_channel | array of String values | :heavy_minus_sign: | Marketing channel that led to the transaction. | | attribution_campaign | array of String values | :heavy_minus_sign: | Marketing campaign that brought the transaction. | | attribution_adgroup | array of String values | :heavy_minus_sign: | Attribution ad group that brought the transaction. | | attribution_adset | array of String values | :heavy_minus_sign: | Attribution ad set that led to the transaction. | | attribution_creative | array of String values | :heavy_minus_sign: | Specific visual or text elements in an ad or campaign tracked to measure effectiveness (e.g., clicks, conversions). | | offer_category | array of String values | :heavy_minus_sign: | Specify the offer categories you want to retrieve data for. Possible values are:
  • introductory
  • promotional
  • winback
| | offer_type | array of String values | :heavy_minus_sign: | Specify the offer types you want to retrieve data for. Possible values are:
  • free_trial
  • pay_as_you_go
  • pay_up_front
| | offer_id | array of String values | :heavy_minus_sign: | Specify the specific offers you want to retrieve data for. | --- # File: export-analytics-api-retrieve-placements.md --- --- title: "Retrieve placement info" description: "" displayed_sidebar: APISidebar --- Retrieves information about paywall or onboarding placements: paywalls, segments, audiences, and A/B tests. This can help you ensure everything is set up correctly without having to open each placement you have separately. :::note As an alternative, you can [export the same data from the dashboard](export-placements.md). ::: ## Endpoint and method ``` POST https://api-admin.adapty.io/api/v1/client-api/exports/placements/ ``` ## Example request ```bash showLineNumbers curl -X POST --location "https://api-admin.adapty.io/api/v1/client-api/exports/placements/" \ -H "Authorization: Api-Key " \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "filters": { "placement_type": "paywall" } }' ``` ```python showLineNumbers url = 'https://api-admin.adapty.io/api/v1/client-api/exports/placements/' headers = { 'Authorization': 'Api-key ', 'Accept': 'text/csv', # or application/json } payload = { 'filters': { 'placement_type': 'paywall', }, } response = requests.post(url, headers=headers, json=payload) ``` ```javascript showLineNumbers const myHeaders = new Headers(); myHeaders.append("Authorization", "Api-Key "); myHeaders.append("Accept", "application/json"); const raw = JSON.stringify({ "filters": { "placement_type": "paywall" } }); const requestOptions = { method: "POST", headers: myHeaders, body: raw, redirect: "follow" }; fetch("https://api-admin.adapty.io/api/v1/client-api/exports/placements/", requestOptions) .then((response) => response.text()) .then((result) => console.log(result)) .catch((error) => console.error(error)); ``` Placeholders: - ``: Your secret API key for authorization. :::tip Depending on how you are going to use the response, you can set `Accept` to `text/csv` to get it as CSV or `application/json` to get a JSON file. ::: ## Parameters | Parameter | Type | Required in request | Nullable in request | Description | |-----------|--------|---------------------|---------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | filters | Object | :heavy_plus_sign: | :heavy_minus_sign: | Filter which placements will be sent in the response.
Currently, you can only filter by `placement_type`: `paywall` or `onboarding`. So, the `filters` parameter value will be an object with one key-value pair. | ```json showLineNumbers { "filters": { "placement_type": "paywall" } } ``` ## Successful response: 200: OK | Parameter | Type | Description | |------------------------------|--------|-----------------------------------------------------------------------| | developer_id | String | Placement ID. | | placement_name | String | Placement name. | | audience_name | String | Audience name. | | segment_name | String | Audience ID. | | cross_placement_ab_test_name | String | Cross-placement A/B test name. Can be `null` in JSON or empty in CSV. | | ab_test_name | String | A/B test name. Can be `null` in JSON or empty in CSV. | | paywall_name | String | Paywall name (only returned if `placement_type` is `paywall`). | | onboarding_name | String | Onboarding name (only returned if `placement_type` is `onboarding`). | ```json { "data": [ { "developer_id": "monthly", "placement_name": "Monthly", "audience_name": "USA", "segment_name": "USA", "cross_placement_ab_test_name": "Monthly cross A/B", "ab_test_name": "Monthly A/B", "paywall_name": "Monthly USA" }, { "developer_id": "weekly", "placement_name": "Weekly", "audience_name": "USA", "segment_name": "USA", "cross_placement_ab_test_name": "Weekly cross A/B", "ab_test_name": "Weekly A/B", "paywall_name": "Weekly USA" } ] } ``` ```csv developer_id,placement_name,audience_name,segment_name,cross_placement_ab_test_name,ab_test_name,paywall_name monthly,Monthly,USA,USA,Monthly cross A/B,Monthly A/B,Monthly USA weekly,Weekly,USA,USA,Weekly cross A/B,Weekly A/B,Weekly USA ``` ## Errors ### 401: Unauthorized

The request failed due to missing or incorrect authorization. Check the [Authorization](ss-authorization) page, paying close attention to the **Authorization header**. The request also failed because the specified profile wasn’t found. #### Body | Parameter | Type | Description | | ----------- | ------- | ------------------------------------------------------------ | | errors | Object |
  • **source**: (string) Always `non_field_errors`.
  • **errors**: A description of the error.
| | error_code | String | Short error name. Always `not_authenticated`. | | status_code | Integer | HTTP status. Always `401.` | #### Response example ```json showLineNumbers { "errors": [ { "source": "non_field_errors", "errors": [ "Authentication credentials were not provided." ] } ], "error_code": "not_authenticated", "status_code": 401 } ``` --- # File: export-analytics-api-retrieve-retention-data.md --- --- title: Retrieve retention data toc_max_heading_level: 2 --- Retrieves the retention data to analyze the ability of a product to keep users engaged over time. ## Endpoint and method ```http POST https://api-admin.adapty.io/api/v1/client-api/metrics/retention/ ``` ## Request example ```bash curl --location 'https://api-admin.adapty.io/api/v1/client-api/metrics/retention/' \ --header 'Authorization: Api-Key ' \ --header 'Content-Type: application/json' \ --data '{ "filters": { "date": [ "2024-01-01", "2024-12-31" ], "compare_date": [ "2023-01-01", "2023-12-31" ] }, "period_unit": "quarter", "period_type": "renewals", "segmentation": "store_product_id", "format": "csv" }' ``` ```python showLineNumbers url = "https://api-admin.adapty.io/api/v1/client-api/metrics/retention/" payload = json.dumps({ "filters": { "date": [ "2024-01-01", "2024-12-31" ], "compare_date": [ "2023-01-01", "2023-12-31" ] }, "period_unit": "quarter", "period_type": "renewals", "segmentation": "store_product_id", "format": "csv" }) headers = { 'Authorization': "Api-Key ", 'Content-Type': "application/json" } response = requests.request("POST", url, headers=headers, data=payload) print(response.text) ``` ```javascript showLineNumbers const myHeaders = new Headers(); myHeaders.append("Authorization", "Api-Key "); myHeaders.append("Content-Type", "application/json"); const raw = JSON.stringify({ "filters": { "date": [ "2024-01-01", "2024-12-31" ], "compare_date": [ "2023-01-01", "2023-12-31" ] }, "period_unit": "quarter", "period_type": "renewals", "segmentation": "store_product_id", "format": "csv" }); const requestOptions = { method: "POST", headers: myHeaders, body: raw, redirect: "follow" }; fetch("https://api-admin.adapty.io/api/v1/client-api/metrics/retention/", requestOptions) .then((response) => response.text()) .then((result) => console.log(result)) .catch((error) => console.error(error)); ``` ## Parameters | Name | Type | Required | Description | | ------------ | ---------------------------------------- | ------------------ | ------------------------------------------------------------ | | filters | [MetricsFilters](#metricsfilters-object) | :heavy_plus_sign: | An object containing filtration parameters. See details below this table. | | period_unit | String | :heavy_minus_sign: |

Specify the time interval for aggregating analytics data, to view results grouped by selected periods, such as days, weeks, months, etc. Possible values are:

  • day
  • week
  • month
  • quarter
  • year
| | segmentation | String | :heavy_minus_sign: |

Sets the basis for segmentation. Possible values are:

  • app_id
  • period
  • renewal_status
  • cancellation_reason
  • store_product_id
  • country
  • store
  • purchase_container_id
  • paywall_id
  • audience_id
  • placement_id
  • attribution_source
  • attribution_status
  • attribution_channel
  • attribution_campaign
  • attribution_adgroup
  • attribution_adset
  • attribution_creative
  • duration
  • default
| | use_trial | Boolean | :heavy_minus_sign: | Boolean indicating whether a trial was used. Defaults to `false`. | | format | String | :heavy_minus_sign: |

Specify the export file format. Available options are:

  • json
  • csv
| ### MetricsFilters object | Name | Type | Required | Description | | ------------------------ | ---------------------------- | ------------------ | ------------------------------------------------------------ | | date | array of String values(data) | :heavy_plus_sign: | Enter the date or period for which you want to retrieve chart data. | | compare_date | array of String values(data) | :heavy_minus_sign: | If comparing two dates or periods, enter the earlier one here and the later one in the `date` parameter. | | store | array of String values | :heavy_minus_sign: | Filter by the app store where the purchase was made. Possible values include **app_store**, **play_store**, **stripe**, and any custom store ID. If using a custom store, enter its ID as set in the Adapty Dashboard. | | country | array of String values | :heavy_minus_sign: | Filter by the 2-letter country code where the purchase took place, using ISO 3166-1 standard codes. | | store_product_id | array of String values | :heavy_minus_sign: | Unique identifier of a product from the app store. You can see this ID in the [**Products**](https://app.adapty.io/products) section of the Adapty Dashboard. | | duration | array of String | :heavy_minus_sign: | Specify the subscription duration. Possible values are:
  • Weekly
  • Monthly
  • 2 months
  • 3 months
  • 6 months
  • Annual
  • Lifetime
  • Uncategorized
| | attribution_source | array of String values | :heavy_minus_sign: | The source integration for attribution. Possible options:
  • adjust
  • airbridge
  • apple_search_ads
  • appsflyer
  • branch
  • custom
| | attribution_status | array of String values | :heavy_minus_sign: | Indicates if the attribution is organic or non-organic. Possible values are:
  • organic
  • non-organic
  • unknown
| | attribution_channel | array of String values | :heavy_minus_sign: | Marketing channel that led to the transaction. | | attribution_campaign | array of String values | :heavy_minus_sign: | Marketing campaign that brought the transaction. | | attribution_adgroup | array of String values | :heavy_minus_sign: | Attribution ad group that brought the transaction. | | attribution_adset | array of String values | :heavy_minus_sign: | Attribution ad set that led to the transaction. | | attribution_creative | array of String values | :heavy_minus_sign: | Specific visual or text elements in an ad or campaign tracked to measure effectiveness (e.g., clicks, conversions). | | offer_category | array of String values | :heavy_minus_sign: | Specify the offer categories you want to retrieve data for. Possible values are:
  • introductory
  • promotional
  • winback
| | offer_type | array of String values | :heavy_minus_sign: | Specify the offer types you want to retrieve data for. Possible values are:
  • free_trial
  • pay_as_you_go
  • pay_up_front
| | offer_id | array of String values | :heavy_minus_sign: | Specify the specific offers you want to retrieve data for. | --- # File: export-analytics-api.md --- --- title: Exporting analytics with API toc_max_heading_level: 2 displayed_sidebar: APISidebar --- 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](export-analytics-api-retrieve-analytics-data) 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](export-analytics-api-retrieve-cohort-data) 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](export-analytics-api-retrieve-conversion-data) 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](export-analytics-api-retrieve-funnel-data) 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](export-analytics-api-retrieve-ltv) 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](export-analytics-api-retrieve-retention-data) method for this. --- **What's next**: - [Authorization and request format](export-analytics-api-authorization) - [Exporting analytics API requests](export-analytics-api-requests) --- # File: export-placements.md --- --- title: "Export placement" description: "Learn how to export placements in Adapty to optimize paywall visibility and user engagement." --- When you work with multiple paywalls and onboardings, it's important to track which ones are shown to which users. You can export all your [placement](placements.md) settings to a CSV file to see which paywall/onboarding appears for each audience and review your setup after making changes or running experiments. :::tip If it's more convenient for you, you can [export placements using the server-side API](export-analytics-api-retrieve-placements). ::: To export paywall/onboarding placements: 1. Go to **[Placements](https://app.adapty.io/placements)** in the main menu. Switch to the **Paywalls** or **Onboardings** tab, as placements for them are exported separately. 2. Click **Export to CSV**. The exported CSV file contains the following information about your placements: - Placement ID - Placement name - Audience name - Segment name - Cross-placement A/B test name - A/B test name - Paywall name --- # File: facebook-ads.md --- --- title: "Facebook Ads" description: "Integrate Facebook Ads with Adapty for effective subscription marketing." --- With the Facebook Ads integration, you can easily check your app stats in Meta Analytics. Adapty sends events to Meta Ads Manager, helping you make similar audiences based on subscriptions to get better returns. This way, you can accurately see how much money your ads are making from subscriptions. The integration between Adapty and Facebook Ads operates in the following way: Adapty sends all subscription events that are configured in your integration to Facebook Ads. This integration is beneficial for evaluating the effectiveness of your advertising campaigns. ## How to set up Facebook Ads integration To integrate Facebook Ads and analyze your app metrics, you can set up the integration with Meta Analytics. By sending events to Meta Ads Manager, you can create lookalike audiences based on subscription events like renewals. To configure this integration, navigate to [Integrations > Facebook Ads](https://app.adapty.io/integrations/facebookanalytics) in the Adapty Dashboard and provide the required credentials. :::note Please consider that the Facebook Ads integration works on iOS 14.5+ only for users with ATT consent. ::: 1. To find App ID, open your app page in [App Store Connect](https://appstoreconnect.apple.com/), go to the **App Information** page in **General** section, and find **Apple ID** in the left bottom part of the screen. 2. You need an application on [Meta for Developers](https://developers.facebook.com/) platform. Log in to your app and then find advanced settings. You can find the **App ID** in the header. 3. Disable client-side tracking in your Meta SDK configuration to prevent double counting of revenue in Meta Ads Manager. You can find this setting in your Meta Developer Console under **App Settings > Advanced Settings**. Set **Log in-app events automatically** to "No". This will ensure that revenue events are only tracked through Adapty's integration. To track install and usage events, you'll need to activate Meta SDK in your code. You can find implementation details in the Meta SDK documentation for your platform: - [iOS SDK](https://developers.facebook.com/docs/ios/getting-started) - [Android SDK](https://developers.facebook.com/docs/android/getting-started) - [Unity SDK](https://developers.facebook.com/docs/unity/getting-started) You can use this integration with Android apps as well. If you set up Android SDK configuration in the **App Settings**, setting up the **Facebook App ID** is enough. ## Events and tags Please note that the Facebook Ads integration specifically caters to companies using Meta for ad campaigns and optimizing them based on customer behavior. It supports Meta's standard events for optimization purposes. Consequently, modifying the event name is not available for the Meta Ads integration. Adapty effectively maps your customer events to their corresponding Meta events for accurate analysis. | Adapty event | Meta Ads event | | :---------------------------- | :-------------------------- | | Subscription initial purchase | Subscribe | | Subscription renewed | Subscribe | | Subscription cancelled | CancelSubscription | | Trial started | StartTrial | | Trial converted | Subscribe | | Trial cancelled | CancelTrial | | Non subscription purchase | fb_mobile_purchase | | Billing issue detected | billing_issue_detected | | Entered grace period | entered_grace_period | | Auto renew off | auto_renew_off | | Auto renew on | auto_renew_on | | Auto renew off subscription | auto_renew_off_subscription | | Auto renew on subscription | auto_renew_on_subscription | StartTrial, Subscribe, CancelSubscription are standard events. To enable specific events, simply toggle on the ones you require. In case multiple event names are selected, Adapty will consolidate the data from all the chosen events into a single Adapty event name. ## SDK configuration :::warning Because of iOS IDFA changes in iOS 14.5, if you use Meta integration, make sure you send `facebookAnonymousId` to Adapty via `.setIntegrationIdentifier()` method. It helps Meta attribute users better. ::: ```swift showLineNumbers do { try await Adapty.setIntegrationIdentifier( key: "facebook_anonymous_id", value: AppEvents.shared.anonymousID ) } catch { // handle the error } ``` ```kotlin showLineNumbers Adapty.setIntegrationIdentifier( "facebook_anonymous_id", AppEventsLogger.getAnonymousAppDeviceGUID(context) ) { error -> if (error != null) { // handle the error } } ``` ```text There is no official SDK for Flutter ``` ```csharp anonymousID is not available in the official SDK https://github.com/facebook/facebook-sdk-for-unity/issues/676 ``` ```typescript showLineNumbers try { const anonymousId = await AppEventsLogger.getAnonymousID(); await adapty.setIntegrationIdentifier("facebook_anonymous_id", anonymousId); } catch (error) { // handle `AdaptyError` } ``` --- # File: fallback-paywalls.md --- --- title: "Fallback paywalls" description: "Use fallback paywalls to ensure seamless user experience in Adapty." --- A paywall is an in-app storefront where customers can see and purchase products within your mobile app. Typically, paywalls are fetched from the server when a customer accesses them. However, Adapty allows you to have fallback paywalls for situations when a user opens the app without a connection to the Adapty backend (e.g., no internet connection or in the rare case of backend unavailability) and there's no cache on the device. Adapty generates fallbacks as a JSON file in the necessary format, showing English versions of the paywalls you've configured in the Adapty Dashboard. To display your fallback paywall to users: 1. Download the file from the Adapty Dashboard - one file per app store and Adapty SDK version - as described below. 2. Process the file in your mobile app code: 1. **For iOS:** Add the file to your Xcode project bundle and pass the file name to the `.setFallback` method. See detailed instructions for [iOS](ios-use-fallback-paywalls). 2. **For Android:** Place the file in the `assets` or `res/raw` directory and pass the file location to the `.setFallback` method. See detailed instructions for [Android](android-use-fallback-paywalls). 3. **For React Native:** Add the file to your Xcode project bundle for iOS and place it in the `android/app/src/main/assets/` or `android/app/src/main/res/raw/` directory for Android, then pass the file location to the `.setFallback` method. See detailed instructions for [React Native](react-native-use-fallback-paywalls). 4. **For Flutter:** Place the file in your app's assets and pass the asset path to the `.setFallback` method. See detailed instructions for [Flutter](flutter-use-fallback-paywalls). 5. **For Unity:** Place the file in your app's StreamingAssets folder and pass the file name to the `.SetFallbackPaywalls` method. See detailed instructions for [Unity](unity-use-fallback-paywalls). ## Download fallback paywalls as a file in the Adapty Dashboard To integrate fallback paywalls into your mobile app code, first download them from the Adapty Dashboard. The downloaded JSON file will contain one paywall for each placement. This will be the paywall assigned to the `All users` audience in the Adapty Dashboard. :::important Downloading fallbacks is available only for Adapty SDK version 2.11 or later. Upgrade to the later version or use existing fallbacks. :::
Before you can download a paywall fallback (Click to Expand) 1. [Create products](create-product) you want to sell 2. [Create a paywall and add the products to it](create-paywall). 3. [Create placement and add paywalls to it](create-placement). Placement is the location where the paywall will be shown.
To download the JSON file with the fallback paywalls: 1. Open the **Paywalls** tab of the **[Placements](https://app.adapty.io/placements)** section from the Adapty main menu. 2. In the **Products** or **Placements** window, click the **Fallbacks** button. 3. Select the platform for the fallbacks - **iOS** or **Android**. 4. In the **Download iOS/Android Fallback** window, select the SDK version you use for your app and click the **Download** button. As a result, you will get a JSON file. --- # File: fetch-paywalls-and-products-android.md --- --- 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." displayed_sidebar: sdkandroid --- 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. ```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`](android-sdk-models#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: ```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`](android-sdk-models#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`](android-sdk-models#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.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: fetch-paywalls-and-products-flutter.md --- --- 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." displayed_sidebar: sdkflutter --- 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. ```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`](flutter-sdk-models#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: ```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`](flutter-sdk-models#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`](flutter-sdk-models#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?.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: fetch-paywalls-and-products-react-native.md --- --- title: "Fetch paywalls and products for remote config paywalls in React Native SDK" description: "Fetch paywalls and products in Adapty React Native SDK to enhance user monetization." displayed_sidebar: sdkreactnative --- 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](react-native-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-reactnative) 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 id = 'YOUR_PLACEMENT_ID'; const locale = 'en'; const paywall = await adapty.getPaywall(id, locale); // the requested paywall } catch (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](react-native-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](react-native-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.

| | **loadTimeoutMs** | 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`](react-native-sdk-models#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 { // ...paywall const products = await adapty.getPaywallProducts(paywall); // the requested products list } catch (error) { // handle the error } ``` Response parameters: | Parameter | Description | | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Products | List of [`AdaptyPaywallProduct`](react-native-sdk-models#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`](react-native-sdk-models#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-react-native#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-react-native#fetch-paywall-information). ::: ```typescript showLineNumbers try { const id = 'YOUR_PLACEMENT_ID'; const locale = 'en'; const paywall = await adapty.getPaywallForDefaultAudience(id, locale); // the requested paywall } catch (error) { // handle the error } ``` :::note The `getPaywallForDefaultAudience` method is available starting from React Native 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](react-native-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: fetch-paywalls-and-products-unity.md --- --- title: "Fetch paywalls and products for remote config paywalls in Unity SDK" description: "Fetch paywalls and products in Adapty Unity SDK to enhance user monetization." displayed_sidebar: sdkunity --- 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](unity-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-unity) 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. ```csharp showLineNumbers Adapty.GetPaywall("YOUR_PLACEMENT_ID", "en", (paywall, error) => { if(error != null) { // handle the error return; } // paywall - the resulting object }); ``` | 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](unity-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](unity-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`](unity-sdk-models#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: ```csharp showLineNumbers Adapty.GetPaywallProducts(paywall, (products, error) => { if(error != null) { // handle the error return; } // products - the requested products array }); ``` Response parameters: | Parameter | Description | | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Products | List of [`AdaptyPaywallProduct`](unity-sdk-models#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`](unity-sdk-models#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?.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. `AdaptySubscriptionPeriodUnit.Day`, `AdaptySubscriptionPeriodUnit.Week`, `AdaptySubscriptionPeriodUnit.Month`, `AdaptySubscriptionPeriodUnit.Year`, or `AdaptySubscriptionPeriodUnit.Unknown`). The `NumberOfUnits` value will get you the number of period units. For example, for a quarterly subscription, you'd see `AdaptySubscriptionPeriodUnit.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-unity#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-unity#fetch-paywall-information). ::: :::note The `getPaywallForDefaultAudience` method is not yet supported in Unity 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](unity-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: fetch-paywalls-and-products.md --- --- 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), [Flutter](flutter-get-pb-paywalls.md), [React Native](react-native-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. ```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`](sdk-models#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`](sdk-models#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://adapty.io/docs/sdk-models#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: ff-action-flow.md --- --- title: "Step 1. Create flow to show paywall data" description: "Set up feature flag action flows in Adapty to personalize user subscription journeys." --- :::important When using the FlutterFlow plugin, you can't use paywalls created in the Adapty Paywall builder. You must implement your own paywall page in FlutterFlow and connect it to Adapty. ::: After adding the Adapty library as a dependency to your FlutterFlow project, it's time to build the flow that **retrieves Adapty paywall and product data and displays it on the paywall you've designed in FlutterFlow**. We first need to receive the paywall data from Adapty. We'll start by requesting the Adapty paywall, then its associated products, and finally checking if the data was successfully received. If successful, we’ll display the product title and price on the paywall page. Otherwise, we'll show an error message. Before proceeding, make sure you've done the following: 1. [Created at least one paywall and added at least one product to it](create-paywall) in the Adapty Dashboard. 2. [Created at last one placement](create-placement) and [added your paywall to it](add-audience-paywall-ab-test) in the Adapty Dashboard. Let's get started! ## Step 1.1. Request Adapty paywall As mentioned, to display data in your FlutterFlow paywall, we first need to retrieve it from Adapty. The initial step is to get the Adapty paywall itself. Here’s how: 1. Open your paywall screen and switch to the **Actions** section in the right pane. There, open the **Action Flow Editor**. 2. In the **Select Action Trigger** window, select **On Page Load**. 3. Click **Add Action**. Then, search for the `getPaywall` custom action and select it. 4. In the **Set Actions Arguments** section, enter the real ID of the [placement you have created](create-placement) in the Adapty Dashboard that includes the paywall. In this example it's `monthly`. Be sure to use your real placement ID! 5. If you have [localized](localizations-and-locale-codes.md) your paywall in the Adapty dashboard, you can also set up the **locale** argument. 6. In the **Action Output Variable Name**, create a new variable and name it `getPaywallResult`. We'll use this in the next step to reference the Adapty paywall and request its products. ## Step 1.2. Request Adapty paywall products Great! We’ve retrieved the Adapty paywall. Now, let's get the products associated with this paywall: 1. Click **+** under the created action and select **Add Action**. This action will receive Adapty paywall products. For this, search and select `getPaywallProducts`. 2. In the **Set Actions Arguments** section, select the `getPaywallResult` variable created earlier. 3. Fill in the other fields as follows: - **Available Options**: Data Structured Field - **Select Field**: value - **Available Options**: No further changes 4. Click **Confirm**. 5. In the **Action Output Variable Name**, create a new variable and name it `getPaywallProductsResult`. We'll use this to map the paywall you designed in FlutterFlow with the Adapty paywall data. ## Step 1.3. Add check if the paywall uploaded successfully Before moving on, let’s verify that the Adapty paywall was received successfully. If so, we can update the paywall with the product data. If not, we’ll handle the error. Here's how to add the check: 1. Click **+** and click **Add Conditional**. 2. In the **Action Output** section, select the action output variable created earlier (`getPaywallResult` in our example). 3. To verify that the Adapty paywall was received, check for the presence of a field with a value. Fill in the fields as follows: - **Available Options**: Has Field - **Field (AdaptyGetPaywallResult)**: value 4. Click **Confirm** to finalize the condition. ## Step 1.4. Log the paywall review To ensure Adapty analytics track the paywall view, we need to log this event. Without this step, the view won’t be counted in the analytics. Here’s how: 1. Click **+** under the **TRUE** label and click **Add Action**. 2. In the **Select Action** field, search for and choose **logShowPaywall**. 3. Click **Value** in the **Set Action Arguments** area and choose the `getPaywallResult` variable we've created. This variable contains the paywall data. 4. Fill in the fields as follows: - **Available Options**: Data Structured Field - **Select Field**: value 5. Click **Confirm**. ## Step 1.5. Show error if paywall not received If the Adapty paywall is not received, you need to [handle the error](error-handling-on-flutter-react-native-unity#system-storekit-codes). In this example, we'll simply display an alert message. 1. Add an **Informational Dialog** action to the **FALSE** label. 2. In the **Title** field, add text you want to see as the dialog title. In this example, it's **Error**. 3. Click **Value** in the **Message** box. 4. Fill in the fields as follows: - **Set Variable**: `getPaywallProductResult` variable we've created - **Available Options**: Data Structure Field - **Select Field**: error - **Available Options**: Data Structure Field - **Select Field**: errorMessage 5. Click **Confirm**. 6. Add a **Terminate action** to the **FALSE** flow. 7. Click **Close** in the top-right corner. Congratulations! You’ve successfully received the product data. Now, let’s [map it to your paywall you've designed in FlutterFlow](ff-add-variables-to-paywalls). --- # File: ff-add-variables-to-paywalls.md --- --- title: "Step 2. Add data to paywall page" description: "Add Feature Flag variables to paywalls in Adapty." --- Once you've [received all the necessary product data](ff-action-flow), it's time to map it to the beautiful paywall you designed in FlutterFlow. In this example, we'll map the product title and its price. ## Step 2.1. Add product title to paywall page 1. Double-click the product text on your paywall page. In the **Set from Variable** window, search for `getPaywallProductResult` variable and choose it. 2. Fill in the fields as follows: - **Available Options**: Data Structured Field - **Select Field**: value - **Available Options**: Item at Index - **List Index Options**: First - **Available Options**: Data Structured Field - **Select Field**: localizedTitle - **Default Variable Value**: null - **UI Builder Display Value**: Anything, in the example, it's `product.title` 3. Click **Confirm** to save the changes. ## Step 2.2. Add price text to paywall page Repeat the steps from Step 2.1 for the price text as shown below: 1. Double-click the price text on your paywall page. In the **Set from Variable** window, search for `getPaywallProductResult` variable and choose it. 2. Fill in the fields as follows: - **Available Options**: Data Structured Field - **Select Field**: value - **Available Options**: Item at Index - **List Index Options**: First - **Available Options**: Data Structured Field - **Select Field**: price - **Default Variable Value**: null - **UI Builder Display Value**: Anything, in the example, it's `product.price` 3. Click the **Confirm** button to save the changes. ### Add price in local currency to paywall page 1. Double-click the price on your paywall page. In the **Set from Variable** window, search for `getPaywallProductResult` variable and choose it. 2. Fill in the fields as follows: - **Available Options**: Data Structured Field - **Select Field**: value - **Available Options**: Item at Index - **List Index Options**: First - **Available Options**: Data Structured Field - **Select Field**: price - **Available Options**: Data Structured Field - **Select Field**: amount - **Available Options**: Decimal - **Decimal Type**: Automatic - **Default Variable Value**: null - **UI Builder Display Value**: Anything, in the example, it's `price.amount` 3. Click **Confirm** to save the changes. And voilà! Now, when you launch your app, it will display the product data from the Adapty paywall directly on your paywall page! It's time to [let your users purchase this product](ff-make-purchase). --- # File: ff-check-subscription-status.md --- --- title: "Step 4. Check access to paid content" description: "Learn how to check subscription status using Adapty's feature flags for better user segmentation." --- When determining if a user has access to specific paid content, you'll need to verify their access level. This means checking if the user has at least one access level, and if that level is the required one. You can do this by checking the user profile, which contains all available access levels. Now, let’s allow users to purchase your product: 1. Double-click the button that should show the paid content and open the **Actions** section in the right pane if it’s not already open. 2. Open the **Action Flow Editor**. 3. In the **Select Action Trigger** window, choose **On Tap**. 4. In the **No Actions Created** window, click the **Add Conditional Action** button. 5. Click **UNSET** to set action arguments and choose the `currentProfile` variable. This is the Adapty variable that holds data about the current user's profile. 6. Fill in the fields as follows: - **Available Options**: Data Structure Field - **Select Field**: accessLevels - **Available Options**: Filter List Items - **Filter Conditions**: 1. Select **Conditions -> Single Condition** and click **UNSET**. 2. In the **First value** field, select **Item in list** as **Source** and fill in the fields as follows: - **Available Options**: Data Structure Field - **Select Field**: accessLevelIdentifier 3. Set the filter operator to **Equal to**. 4. Click **UNSET** next to **Second value** and in the **Value** field, enter the ID of your access level; in our example we use `premium`. 5. Click **Confirm** and continue filling in the other fields below. - **Available Options**: Item at Index - **List Index Options**: First - **Available Options**: Data Structure Field - **Select Field**: accessLevel - **Available Options**: Data Structure Field - **Select Field**: isActive 7. Click **Confirm**. Now, add the actions for what happens next — if the user has the right subscription or not. Either take them to the page available to premium subscribers or open the paywall page so they can buy access. --- # File: ff-getting-started.md --- --- title: "Getting started" description: "Get started with Adapty Feature Flags to personalize subscription flows." --- With Adapty, you can create and run paywalls and A/B tests at different points in your mobile app user's journey, such as Onboarding, Settings, etc. These points are called [Placements](placements). A placement in your app can manage multiple paywalls or [A/B tests](ab-tests) at a time, each made for a certain group of users, which we call [Audiences](audience). Moreover, you can experiment with paywalls, replacing one with another over time without releasing a new app version. The only thing you hardcode in the mobile app is the placement ID. The Adapty library keeps your paywall updated with the latest products from your Adapty Dashboard. It [fetches the product data](ff-action-flow) and [shows it on your paywall](ff-add-variables-to-paywalls), [handles purchases](ff-make-purchase), and [checks the user’s access level](ff-check-subscription-status) to see if they should get paid content. To get started, just [add the Adapty library](ff-getting-started#add-the-adapty-plugin-as-a-dependency) to your FlutterFlow project and [initiate it](ff-getting-started#initiate-adapty-plugin) as shown below. :::warning Before you start, note the following limitations: - The Adapty library for FlutterFlow doesn’t support web apps. Avoid compiling web apps with it. - The Adapty library for FlutterFlow doesn't support paywalls creating using the Adapty paywall builder. You need to design your own paywall in FlutterFlow before enabling purchases with Adapty. ::: ## Add the Adapty library as a dependency 1. In the [FlutterFlow Dashboard](https://app.flutterflow.io/dashboard), open your project, and then click **Settings and Integrations** from the left menu. In the **Project setup** section on the left, select **Project dependencies**. 2. In the **FlutterFlow Libraries** section, click **Add Library** and enter `adapty-xtuel0`. Click **Add**. 3. Now, you need to associate your SDK key with the library. Click **View details** next to the library. 4. Copy the **Public SDK key** from the [**App Settings** -> **General** tab](https://app.adapty.io/settings/general) in the Adapty Dashboard. 5. Paste the key to **AdaptyApiKey** in FlutterFlow. The Adapty FF library will now be added as a dependency to your project. In the **Adapty** FF library window, you’ll find all the Adapty resources that have been imported into your project. ## Call the new activation action at application launch 1. Go to **Custom Code** section from the left menu and open `main.dart`. 2. Click **+** and select `activate (Adapty)`. 3. Click **Save**. ## Initiate Adapty plugin For the Adapty Dashboard to recognize your app, you’ll need to provide a special key in FlutterFlow. 1. In your FlutterFlow project, go to **Settings and Integrations > Permissions** from the left menu. 2. In the opened **Permissions** window, click the **Add Permission** button. 3. In both the **iOS Permission Key** and **Android Permission Key** field, paste `AdaptyPublicSdkKey`. 4. For the **Permission Message**, copy the **Public SDK key** from the [**App Settings** -> **General** tab](https://app.adapty.io/settings/general) in the Adapty Dashboard. Each app has its own SDK key, so if you have multiple apps, make sure you grab the right one. After completing these steps, you'll be able to call your paywall in your FlutterFlow app and enable purchases through it. ## What's next? 1. [Create an action flow](ff-action-flow) for handling Adapty paywall products and their data in FlutterFlow. 2. [Map the received data to the paywall](ff-add-variables-to-paywalls) you designed in FlutterFlow. 3. [Set up the purchase button](ff-make-purchase) on your paywall to process transactions through Adapty when clicked. 4. Finally, [add subscription status checks](ff-check-subscription-status) to determine whether to display paid content to the user. --- # File: ff-make-purchase.md --- --- title: "Step 3. Enable purchase" description: "Learn how to make purchases using Adapty’s Feature Flags system." --- Congratulations! You've successfully [set up your paywall to display product data from Adapty](ff-add-variables-to-paywalls), including the product title and price. Now, let's move on to the final step – letting users make a purchase through the paywall. ## Step 3.1. Enable users to make purchases 1. Double-click the buy button on your paywall page. In the right panel, open the **Actions** section if it's not already open. 2. Open the **Action Flow Editor**. 3. In the **Select Action Trigger** window, choose **On Tap**. 4. In the **No Actions Created** window, click **Add Action**. Search for the `makePurchase` action and choose it. 5. In the **Set Actions Arguments** section, choose `getPaywallProductsResult` variable created earlier. 6. Fill in the fields as follows: - **Available Options**: Data Structure Field - **Select Field**: value - **Available Options**: Item at Index - **List Index Options**: First 7. Click `subscriptionUpdateParameters`, search for `AdaptySubscriptionUpdateParameters` and select it. Click **Confirm**. :::info By default, you can leave all the object fields empty. You would need to fill them in to replace one subscription with another in Android apps. Read more [here](sdk-models#adaptysubscriptionupdateparameters). ::: 8. Click **Confirm**. 9. In the **Action Output Variable Name**, create a new variable and name it `makePurchaseResult` - this will be used later to confirm the purchase was successful. ## Step 3.2. Check if the purchase was successful Now, let's set up a check to see if the purchase went through. 1. Click **+** and click **Add Conditional**. 2. In **Set Condition for Action**, select the `makePurchaseResult` variable. 3. In the **Set Variable** window, fill in the fields as follows: - **Available Options**: Has Field - **Select Field**: profile 4. Click **Confirm**. ## Step 3.3. Open paid content If the purchase is successful, you can unlock the paid content. Here’s how to set that up: 1. Click **+** under the **TRUE** label and click **Add Action**. 2. In the **Define Action** field, search for and select the page you want to open from the **Navigate To** list. In this example, the page is **Questions**. ## Step 3.4 Show error message if purchase failed If the purchase fails, let's display an alert to the user. 1. Add an **Informational Dialog** action to the **FALSE** label. 2. In the **Title** field, enter the text you want for the dialog title, such as **Purchase Failed**. 3. Click **Value** in the **Message** box. In the **Set from Variable** window, search for `makePurchaseResult` and choose it. Fill in the fields as follows: - **Available Options**: Data Structure Field - **Select Field**: error - **Available Options**: Data Structure Field - **Select Field**: errorMessage 4. Click **Confirm**. 5. Add a **Terminate** action to the **FALSE** flow. 6. Finally, click **Close** in the top-right corner. Congratulations! Your users can now purchase your products. As an extra step, let's [set up a check for user access to paid content](ff-check-subscription-status) elsewhere to decide whether to display paid content or the paywall to them. --- # File: ff-resources.md --- --- title: "Adapty FlutterFlow plugin actions and data types" description: "Access Adapty's feature flag resources to streamline subscription-based features." --- ## Custom Actions Below are Adapty methods delivered to FlutterFlow with Adapty plugin. They can be used as custom actions in FlutterFlow. | Custom Action | Description | Action Arguments | Adapty Data Types - Action Output Variable | |---|----|--------|----| | activate | Initializes the Adapty SDK | None || |

getPaywall

| Retrieves a paywall. It does not return paywall products. Use the `getPaywallProducts` action to get the actual products |
  • [Placement_ID](placements)
  • [Locale](localizations-and-locale-codes)
| [AdaptyGetPaywallResult](ff-resources#adaptygetpaywallresult)| |

getPaywallProducts

| Returns a list of actual paywall products | [AdaptyPaywall](ff-resources#adaptypaywall) | [AdaptyGetProductsResult](ff-resources#adaptygetproductsresult) | |

getProductsIntroductoryOfferEligibility

| Checks if the user qualifies for an introductory iOS subscription offer | [AdaptyPaywallProduct](product) | [AdaptyGetIntroEligibilitiesResult](ff-resources#adaptygetintroeligibilitiesresult) | |

makePurchase

| Completes a purchase and unlocks content. If a paywall has a promotional offer, Adapty automatically applies it at checkout|
  • **product**: an AdaptyPaywallProduct object retrieved from the paywall.
  • **subscriptionUpdateParams**: an [`AdaptySubscriptionUpdateParameters`](ff-resources#adaptysubscriptionupdateparameters) object used to upgrade or downgrade a subscription (use for Android).
  • **isOfferPersonalized**: Specifies whether the offer is personalized to the buyer (use for Android).
| [AdaptyMakePurchaseResult](ff-resources#adaptymakepurchaseresult) | |

getProfile

|

Retrieves the current app user's profile. This allows you to set access levels and other parameters

If it fails (e.g., due to no internet), cached data will be returned. Adapty regularly updates the profile cache to ensure the information stays as current as possible

| None| [AdaptyGetProfileResult](ff-resources#adaptygetprofileresult) | | updateProfile | Changes optional attributes of the current user profile such as email, phone number, etc. You can later use attributes to create user [segments](segments) or just view them in CRM | The ID and any parameters that need to be updated for the [AdaptyProfile](ff-resources#adaptyprofile) | [AdaptyError](ff-resources#adaptyerror) (Optional) | | restorePurchases | Restores any purchases the user has made | None | [AdaptyGetProfileResult](ff-resources#adaptygetprofileresult) | | logShowPaywall | Logs when a specific paywall is shown to the user | [AdaptyPaywall](ff-resources#adaptypaywall) | [AdaptyError](ff-resources#adaptyerror) (Optional) | | logShowOnboarding |

Tracks users' steps during the onboarding process.

The onboarding stage is a crucial part of modern mobile apps. The effectiveness of its implementation, the quality of the content, and the number of steps can significantly impact user behavior, particularly their willingness to subscribe or make purchases. To help you analyze user behavior during this critical phase without leaving Adapty, we’ve added the capability to send dedicated events every time a user navigates to a new onboarding screen.

|
  • **name**: (String) The name of your onboarding process.
  • **screenName**: (String) A descriptive name for a specific screen within the onboarding.
  • **order**: (Integer) An unsigned integer value representing the order of this screen in your onboarding sequence (it must me greater than 0).
| [AdaptyError](ff-resources#adaptyerror) (Optional) | | identify | Identifies the user using your system's `customerUserId` | customerUserId | [AdaptyError](ff-resources#adaptyerror) (Optional) | | logout | Logs the current user out of your app | None | [AdaptyError](ff-resources#adaptyerror) (Optional)| | presentCodeRedemptionSheet | Displays a sheet that allows users to redeem codes (iOS only) | None | None | ## Data Types Adapty data types (collections of data values) delivered to FlutterFlow with Adapty plugin. ### AdaptyAccessLevel Information about the user's [access level](access-level). | Field Name | Type | Description | |--------------------------|----------|-------------| | activatedAt | DateTime | The time when this access level was activated | | activeIntroductoryOfferType | String | The type of an active introductory offer. If set, it means an offer was applied during this subscription period | | activePromotionalOfferId | String | The ID of an active promotional offer (purchased from iOS) | | activePromotionalOfferType | String | The type of an active promotional offer (purchased from iOS). If set, it means an offer was applied during this subscription period | | billingIssueDetectedAt | DateTime | The time when a billing issue was detected. The subscription can still be active. Set to null if payment is successfully processed | | cancellationReason | String | The reason why the subscription was canceled | | expiresAt | DateTime | The access level expiration time (could be in the past or not set for lifetime access) | | id | String | The identifier of the access level | | isActive | Boolean | True if this access level is active. Generally, you can check this property to determine if a user has an access to premium features | | isInGracePeriod | Boolean | True if this auto-renewable subscription is in the [grace period](https://developer.apple.com/help/app-store-connect/manage-subscriptions/enable-billing-grace-period-for-auto-renewable-subscriptions) | | isLifetime | Boolean | True if this access level is active for a lifetime (no expiration date) | | isRefund | Boolean | True if this purchase was refunded | | offerId | String | The ID of an active promotional offer (purchased from Android) | | renewedAt | DateTime | The time when the access level was last renewed | | startsAt | DateTime | The start time of this access level (could be in the future) | | store | String | The store where the purchase was made | | unsubscribedAt | DateTime | The time when auto-renewal was turned off for the subscription. The subscription can still be active. If not set, the user reactivated the subscription | | vendorProductId | String | The product ID from the store that unlocked this access level | | willRenew | Boolean | True if this auto-renewable subscription is set to renew | ### AdaptyAccessLevelIdentifiers This struct is intended to replace key-value pair for `Map | List of the user's eligibility for promotional offers | | error | Data ([AdaptyError](ff-resources#adaptyerror)) | Contains details about the error via [`AdaptyError`](ff-resources#adaptyerror) | ### AdaptyGetPaywallResult Contains the result of the `getPaywall` custom action. | Field Name | Type | Description | |--------------------------|----------|-------------| | value | Data ([AdaptyPaywall](ff-resources#adaptypaywall)) | Contains a list of [AdaptyPaywall](ff-resources#adaptypaywall) objects | | error | Data ([AdaptyError](ff-resources#adaptyerror)) | Contains error information via [AdaptyError](ff-resources#adaptyerror) | ### AdaptyGetProductsResult Contains the result of the `getPaywallProducts` custom action. | Field Name | Type | Description | |--------------------------|----------|-------------| | value | List < Data ([AdaptyPaywallProduct](product)) > | Contains a list of [AdaptyPaywallProducts](product) | | error | Data ([AdaptyError](ff-resources#adaptyerror)) | Contains error information via [AdaptyError](ff-resources#adaptyerror) | ### AdaptyGetProfileResult Contains the result of the `getProfile` custom action. | Field Name | Type | Description | |--------------------------|----------|-------------| | value | Data ([AdaptyProfile](ff-resources#adaptyprofile)) | Contains the user profile as an [AdaptyProfile](ff-resources#adaptyprofile) | | error | Data (AdaptyError) | Contains error information via [AdaptyError](ff-resources#adaptyerror) | ### AdaptyMakePurchaseResult Contains the result of the `makePurchase` custom action. | Field Name | Type | Description | |--------------------------|----------|-------------| | value | Data ([AdaptyProfile](ff-resources#adaptyprofile)) | Contains the user's profile as an [AdaptyProfile](ff-resources#adaptyprofile) | | error | Data ([AdaptyError](ff-resources#adaptyerror)) | Contains error information via [AdaptyError](ff-resources#adaptyerror) | ### AdaptyNonSubscription Information about non-subscription purchases. These can be one-time (consumable) products, unlocks (like new map unlock in the game), etc. | Field Name | Type | Description | |--------------------------|----------|-------------| | isConsumable | Boolean | Indicates whether the product is consumable | | isOneTime | Boolean | Indicates if the product is a one-time purchase (e.g., if true, the purchase is processed only once) | | isRefund | Boolean | Indicates if the product has been refunded | | isSandbox | Boolean | Indicates if the product was purchased in a sandbox environment | | purchasedAt | DateTime | The time when the product was purchased | | purchaseId | String | The ID of the purchase in Adapty. Can be used for tracking one-time products | | store | String | The store where the product was purchased (e.g., App Store, Google Play) | | vendorProductId | String | ID of the product in vendor's system | | vendorTransactionId | String | Transaction ID in the vendor's system | ### AdaptyPaywall Information about a [paywall](paywalls). | Field Name | Type | Description | |----------------------|----------|-------------| | abTestName | String | The name of the parent A/B test | | hasViewConfiguration | Boolean | Indicates if there is a view configuration for the paywall | | locale | String | The locale ID of the paywall | | name | String | Paywall name | | placement.id | String | The ID of the parent placement | | remoteConfigString | String | A custom dictionary from Adapty Dashboard associated with this paywall | | placement.revision | Integer | The current revision/version of the paywall. Every change generates a new revision | | variationId | String | The variation ID used to attribute purchases to this paywall | | vendorProductIds | String | Array of product IDs related to the paywall | ### AdaptyPaywallProduct Information about [product](product). | Field Name | Type | Description | | -------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | vendorProductId | String | The ID of a product from an app store | | localizedDescription | String | A description of the product in the user's language | | localizedTitle | String | The name of the product in the user's language | | regionCode | String | The region code of the locale used to format the price of the product (use for iOS) | | isFamilyShareable | Boolean | A Boolean value that indicates whether the product is available for family sharing in App Store Connect. Will be always FALSE for iOS version below 14.0 and macOS version below 11.0 (use for iOS) | | paywallVariationId | String | The ID of a variation, used to attribute purchases to this paywall | | paywallABTestName | String | Parent A/B test name | | paywallName | String | Parent paywall name | | price | Data ([AdaptyPriceData](#adaptyprice) | The price of the product | | subscriptionDetails | Data ([AdaptySubscriptionDetails](#adaptysubscriptiondetails)) | Information on subscription | ### AdaptyPrice Information about product price. | Field Name | Type | Description | | --------------- | ------ | ------------------------------------------ | | amount | Double | The numeric value of the price | | currencyCode | String | The code of the price currency | | currencySymbol | String | The symbol used for the currency | | localizedString | String | The price displayed in the user's language | ### AdaptyProductIntroEligibility Defines if the user qualifies for an introductory offer for an iOS subscription. | Field Name | Type | Description | | --------------- | ----------------------------------------------------------- | ------------------------------------------------------------ | | vendorProductId | String | The ID of a product from an app store | | eligibility | [AdaptyEligibilityEnum](ff-resources#adaptyeligibilityenum) | Definition if the user qualifies for an introductory offer for an iOS subscription | ### AdaptyProductNonsubscriptions Details of the active non-subscription tied to this product. | Field Name | Type | Description | | ---------------- | ----------------------------------------------------------- | ------------------------------------------------------------ | | productId | String | The ID of the product from an app store | | nonsubscriptions | [AdaptyNonSubscription](ff-resources#adaptynonsubscription) | Information about non-subscription purchases. These can be one-time (consumable) products, unlocks (like new map unlock in the game), etc. | ### AdaptyProductSubscriptions Details of the active subscription tied to this product. | Field Name | Type | Description | | ------------ | ----------------------------------------------------- | ---------------------------------------- | | productId | String | The ID of the product from an app store | | subscription | [AdaptySubscription](ff-resources#adaptysubscription) | Information about subscription purchases | ### AdaptyProfile Information on the user's profile | Field Name | Type | Description | | ---------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | accessLevels | List < Data ([AdaptyAccessLevelIdentifiers](ff-resources#adaptyaccesslevelidentifiers)) > | List of all access levels that belong to the user | | profileId | String | The ID of the user profile | | customerUserId | String | The ID of the user in the vendor's system | | subscriptions | List < Data ([MapKeySubscriptions](#mapkeysubscriptions)) > | The list of all subscriptions purchased by the user | | nonSubscriptions | List < Data ([MapKeyNonSubscriptions](#mapkeynonsubscriptions)) > | The list of all non-subscription products purchased by the user | ### AdaptyProfileParameters Information on the user. | Field Name | Type | Description | | ----------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | firstName | String | The first name of the user | | lastName | String | The last name of the user | | gender | [AdaptyGenderEnum](#adaptygenderenum) | The gender of the user | | birthday | String | The birthday of the user | | email | String | The email of the user | | phoneNumber | String | The phone number of the user | | facebookAnonymousId | String | The ID of the user in [Facebook Ads integration](facebook-ads) | | amplitudeUserId | String | The ID of the user in [Amplitude integration](amplitude) | | amplitudeDeviceId | String | The ID of the user's device in [Amplitude integration](amplitude) | | mixpanelUserId | String | The ID of the user in [Mixpanel integration](mixpanel) | | appmetricaProfileId | String | The ID of the user in [AppMetrica integration](appmetrica) | | appmetricaDeviceId | String | The ID of the user's device in [AppMetrica integration](appmetrica) | | oneSignalPlayerId | String | The ID of the user in [OneSignal integration](onesignal) | | pushwooshHWID | String | The ID of the user's device in [Pushwoosh integration](pushwoosh) | | firebaseAppInstanceId | String | The ID of the user in [Firebase integration](firebase-and-google-analytics) | | airbridgeDeviceId | String | The ID of the user's device in [Airbridge integration](airbridge) | | appTrackingTransparencyStatus | AdaptyATTStatus | The status of the access to IDFA (use for iOS) | | analyticsDisabled | Boolean | Definition if the external [analytics is opted out for the user](analytics-integration#disabling-external-analytics-for-a-specific-customer) | | customStringAttributes | List < Data ([AdaptyCustomStringAttribute](ff-resources#adaptycustomstringattribute)) > | List of custom string attributes of the user | | customDoubleAttributes | List < Data ([AdaptyCustomDoubleAttribute](ff-resources#adaptycustomdoubleattribute)) > | List of custom double attributes of the user | ### AdaptySubscription Information on existing user subscription. | Field Name | Type | Description | | --------------------------- | -------- | ------------------------------------------------------------ | | activatedAt | DateTime | The time when this subscription was activated | | activeIntroductoryOfferType | String | The type of an active introductory offer. If set, it means an offer was applied during this subscription period | | activePromotionalOfferId | String | The ID of an active promotional offer (use for iOS) | | activePromotionalOfferType | String | The type of an active promotional offer (use for iOS). If set, it means an offer was applied during this subscription period | | cancellationReason | String | The reason why the subscription was canceled | | expiresAt | DateTime | The subscription expiration time | | renewedAt | DateTime | The time when the subscription was last renewed | | unsubscribedAt | DateTime | The time when auto-renewal was turned off for the subscription. The subscription can still be active. If not set, the user reactivated the subscription | | billingIssueDetectedAt | DateTime | The time when a billing issue was detected. The subscription can still be active. Set to null if payment is successfully processed | | isActive | Boolean | True if this subscription is active. Generally, you can check this property to determine if a user has access to premium features | | isInGracePeriod | Boolean | True if this auto-renewable subscription is in the [grace period](https://developer.apple.com/help/app-store-connect/manage-subscriptions/enable-billing-grace-period-for-auto-renewable-subscriptions) | | isLifetime | Boolean | True if this subscription is active for a lifetime (no expiration date) | | isRefund | Boolean | True if this purchase was refunded | | isSandbox | Boolean | Indicates if the product was purchased in a sandbox environment | | offerId | String | The ID of an active promotional offer (use for Android) | | startsAt | DateTime | The start time of this access level (could be in the future) | | store | String | The store where the product was purchased (e.g., App Store, Google Play) | | vendorOriginalTransactionId | String | ID of the initial subscription in vendor's system | | vendorProductId | String | ID of the product in vendor's system | | vendorTransactionId | String | Transaction ID in the vendor's system | | willRenew | Boolean | True if this auto-renewable subscription is set to renew | ### AdaptySubscriptionDetails Scheme of a Subscription object as a part of the [AdaptyPaywallProduct](product). | Field Name | Type | Description | | ----------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | androidBasePlanId | String | [Base plan ID](https://support.google.com/googleplay/android-developer/answer/12154973) in the Google Play Store or [price ID](https://docs.stripe.com/products-prices/how-products-and-prices-work#what-is-a-price) in Stripe. | | androidIntroductoryOfferEligibility | [AdaptyEligibilityEnum](ff-resources#adaptyeligibilityenum) | Definition if the user qualifies for an introductory offer for an iOS subscription | | androidOfferId | String | The ID of an active promotional offer (use for Android) | | androidOfferTags | List < String > | List of [custom tags](https://developers.google.com/android-publisher/api-ref/rest/v3/OfferTag) specified for base plans and subscription offers. | | introductoryOffer | List < Data ([AdaptySubscriptionPhase](ff-resources#adaptysubscriptionphase)) > | The ID of an introductory offer (use for iOS) | | localizedSubscriptionPeriod | String | The period of the subscription in the user's language | | promotionalOffer | Data ([AdaptySubscriptionPhase](ff-resources#adaptysubscriptionphase)) | The promotional offer details (use for iOS) | | promotionalOfferEligibility | Boolean | Definition if the user qualifies for an promotional offer for an iOS subscription | | promotionalOfferId | String | The ID of the promotional offer (use for iOS) | | renewalType | [AdaptyRenewalTypeEnum](#adaptyrenewaltypeenum) | Defines if the subscription is auto-renewable or not via [AdaptyRenewalTypeEnum](ff-resources#adaptyrenewaltypeenum) | | subscriptionGroupIdentifier | String | The ID of the product group the product belongs to (use for iOS) | | subscriptionPeriod | Data ([AdaptySubscriptionPeriod](#adaptysubscriptionperiod)) | The duration of the subscription | ### AdaptySubscriptionPeriod The duration of the subscription. | Field Name | Type | Description | | ------------- | --------------------------------------------- | ----------------------------------------------------------- | | numberOfUnits | Integer | Number of days/weeks/months/years the subscription lasts. | | unit | [AdaptyPeriodUnitEnum](#adaptyperiodunitenum) | Measurement unit of the period: days, weeks, months, years. | ### AdaptySubscriptionPhase Represents a subscription phase, such as a free trial or an introductory offer period. | Field Name | Type | Description | | --------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | identifier | String | The ID of the phase | | localizedNumberOfPeriods | String | The length of the phase. For example, a 6-month offer would display as `6 months` in the user's language. | | localizedSubscriptionPeriod | String | The subscription duration in the user's language, like `3 months`. | | numberOfPeriods | Integer | The number of subscription periods in this phase. For instance, a 6-month offer would have two 3-month periods. | | paymentMode | [AdaptyPaymentModeEnum](#adaptypaymentmodeenum) | The payment model used for this phase. | | price | Data ([AdaptyPrice](#adaptyprice)) | The price of this phase. | | subscriptionPeriod | Data ([AdaptySubscriptionPeriod](#adaptysubscriptionperiod)) | The subscription period on which this phase is based. | ### AdaptySubscriptionUpdateParameters (*Android only*) Parameters for replacing one subscription with another. | Field Name | Type | Description | | ---------- | ------------------------------------------------------------ | ---------- | | oldSubVendorProductId | String | The ID of the current subscription in the Play Store that you want to replace. | | replacementMode | [AdaptySubscriptionUpdateReplacementMode](ff-resources#adaptysubscriptionupdatereplacementmode) | Enum that corresponds to [`BillingFlowParams.ProrationMode`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode) values. | ### MapKeyNonSubscriptions Replacement for a dictionary for [AdaptyNonSubscription](ff-resources#adaptynonsubscription). | Field Name | Type | | ---------- | ------------------------------------------------------------ | | key | String | | value | List < Data ([AdaptyNonSubscription](ff-resources#adaptynonsubscription)) > | ### MapKeySubscriptions Replacement for a dictionary for [AdaptySubscription](ff-resources#adaptysubscription). | Field Name | Type | | ---------- | ------------------------------------------------------------ | | key | String | | value | List < Data ([AdaptySubscription](ff-resources#adaptysubscription)) > | ## Enums Adapty enums (variables that are sets of predefined constants) delivered to FlutterFlow with the Adapty plugin. ### AdaptyEligibilityEnum Defines if the user qualifies for an introductory offer for an iOS subscription. | Field Name | Description | |--------------------------|-------------| | eligible | The user is eligible for an intro offer, it is safe to reflect this info in your UI | | ineligible | The user is not eligible to get any offer, you shouldn't present it in your UI | | notApplicable | This product is not configured to have an offer | ### AdaptyGenderEnum Defines user's gender. | Field Name | Description | | ---------- | -------------------------------------------- | | none | The gender is not set | | female | User's gender is female | | male | User's gender is male | | Other | The user has defined their gender as "other" | ### AdaptyPaymentModeEnum Defines the payment model. | Field Name | Description | | ---------- | ------------------------------------------------------------ | | payAsYouGo | A pricing model where customers are billed based on their actual usage or consumption of a product/service, rather than paying for a fixed fee upfront | | payUpFront | A pricing model where customers are billed before they receive the product/service. | | freeTrial | User is on a free trial period | | unknown | Pricing model is not defined | ### AdaptyPeriodUnitEnum Defines the units in which the periods are measured. | Field Name | Description | | ---------- | ----------- | | day | In days | | week | In weeks | | month | In months | | year | In years | | unknown | Not defined | ### AdaptyRenewalTypeEnum Defines if the subscription is auto-renewable or not. | Field Name | Description | | ------------- | --------------------------------------------------- | | prepaid | The subscription is prepaid and not auto-renewable. | | autorenewable | The subscription is auto-renewable | ### AdaptySubscriptionUpdateReplacementMode Defines the subscription update mode for Android. | Field Name | Description | | ------------- | --------------------------------------------------- | | withTimeProration | (default) The new plan takes effect immediately, and the remaining time will be prorated and credited to the user. | | chargeProratedPrice | The new plan takes effect immediately, and the billing cycle remains the same. The price for the remaining period will be charged. This option is only available for subscription upgrades. | | withoutProration | The new plan takes effect immediately, and the new price will be charged on the next recurrence time. The billing cycle stays the same. | | deferred | The new purchase takes effect immediately, and the new plan will take effect when the old item expires. | | chargeFullPrice | The new plan takes effect immediately, and the billing cycle remains the same. The price for the remaining period will be charged. This option is only available for subscription upgrades. | ### App States App state variables are specific variables that hold the current state of an application. They can be accessed and modified throughout the entire application across all pages and components. This type of variable can be useful for storing data that needs to be shared between different parts of the app, such as user preferences and authentication tokens. | Field Name | Data Type | Persisted | Description | | -------------- | -------------------------------------------------- | --------- | ------------------------------------------------------------ | | currentProfile | Data ([AdaptyProfile](ff-resources#adaptyprofile)) | False | The variable that contains the information on the current user profile. Keep it up-to-date. | --- # File: firebase-and-google-analytics.md --- --- title: "Firebase and Google Analytics" description: "Integrate Firebase and Google Analytics with Adapty for better insights." --- If you use such Google products as Google Analytics, Firebase, and BigQuery you may enrich your analytical data with events from Adapty using the integration described in this article. Events are sent through Google Analytics to Firebase and may be used in any of these services. ## How to set up Firebase integration ### 1\. Set up Firebase First of all, you have to enable integration between Firebase and Google Analytics. You can do it in your Firebase Console in the **Integrations** tab. ### 2\. Integrate with Adapty Then Adapty needs your Firebase App ID and Google Analytics API Secret to send events and user properties. You can find these parameters in the Firebase Console and Google Analytics Data Streams Tab respectively. Next, access the App's Stream details page within the Data Streams section of Admin settings in [Google Analytics.](https://analytics.google.com/analytics/web/#/) Under **Additional settings**, go to the **Measurement Protocol API secrets** page and create a new **API Secret** if it doesn't exist. Copy the value. Then, your next step will be adjusting integration in Adapty Dashboard. You will need to provide Firebase App ID and Google Analytics API Secret to us for your iOS and Android platforms. ## SDK configuration Then you have to set up Adapty SDK to associate your users with Firebase. For each user, you should send the `firebase_app_instance_id` to Adapty. Here you can see an example of the code that can be used to integrate Firebase SDK and Adapty SDK. ```swift showLineNumbers FirebaseApp.configure() if let appInstanceId = Analytics.appInstanceID() { do { try await Adapty.setIntegrationIdentifier( key: "firebase_app_instance_id", value: appInstanceId ) } catch { // handle the error } } ``` ```kotlin showLineNumbers //after Adapty.activate() FirebaseAnalytics.getInstance(context).appInstanceId.addOnSuccessListener { appInstanceId -> Adapty.setIntegrationIdentifier("firebase_app_instance_id", appInstanceId) { error -> if (error != null) { // handle the error } } } ``` ```java showLineNumbers //after Adapty.activate() FirebaseAnalytics.getInstance(context).getAppInstanceId().addOnSuccessListener(appInstanceId -> { Adapty.setIntegrationIdentifier("firebase_app_instance_id", appInstanceId, error -> { if (error != null) { // handle the error } }); }); ``` ```javascript showLineNumbers final appInstanceId = await FirebaseAnalytics.instance.appInstanceId; try { await Adapty().setIntegrationIdentifier( key: "firebase_app_instance_id", value: appInstanceId, ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` ```csharp showLineNumbers using AdaptySDK; // We suppose FirebaseAnalytics Unity Plugin is already installed Firebase.Analytics .FirebaseAnalytics .GetAnalyticsInstanceIdAsync() .ContinueWithOnMainThread((task) => { if (!task.IsCompletedSuccessfully) { // handle error return; } var firebaseId = task.Result var builder = new Adapty.ProfileParameters.Builder(); Adapty.SetIntegrationIdentifier( "firebase_app_instance_id", firebaseId, (error) => { // handle the error }); }); ``` ```typescript showLineNumbers try { const appInstanceId = await analytics().getAppInstanceId(); await adapty.setIntegrationIdentifier("firebase_app_instance_id", appInstanceId); } catch (error) { // handle `AdaptyError` } ``` ## Sending events and user properties And now it is time to decide which events you will receive in Firebase and Google Analytics. You can see that some events have designated names, for example. "Purchase", while other ones are usual Adapty events. This discrepancy comes from[ Google Analytics event types](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference/events). Currently, supported events are [Refund](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference/events#refund%22%3ERefund) and [Purchase](https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference/events#purchase%22%3EPurchase). Other events are custom events. So, please ensure that your event names are [supported](https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=firebase#limitations%22%3E) by Google Analytics. Also, you can set up sending user properties in the Adapty dashboard. This means that your events will be enriched with `subscription_state` and `subscription_product_id` by Adapty. But you also have to [enable](https://support.google.com/analytics/answer/10075209?hl=en) this feature in Google Analytics. So to use **User properties** in your analytics, begin by assigning them to a custom dimension through the Firebase Console's **Custom Definitions** by selecting the **User scope**, naming, and describing them. Please check that your user property names are `subscription_state` and `subscription_product_id`. Otherwise, we won't be able to send you subscription status data. :::note There is a time delay between when events are sent from Adapty and when they appear on the Google Analytics Dashboard. It's suggested to monitor the Realtime Dashboard on your Google Analytics account to see the latest events in real-time. ::: And that's all! Wait for new insights from Google. --- # File: firebase-apps.md --- --- title: "Firebase apps" description: "Integrate Firebase with Adapty to enhance user analytics and subscription tracking for your mobile app." --- This page is about integration of Adapty in you app, if it works on Firebase. :::note Get started This is not all steps required for Adapty to work, just some useful tips for integration with Firebase. If you want to integrate Adapty in you app, you should read [Quickstart Guide](quickstart) first ::: ## User Identification If you're using Firebase auth, this snippet may help you keep your users in sync between Firebase and Adapty. Note that it's just an example, and you should consider your app auth specifics. ```swift showLineNumbers @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Configure Adapty before Firebase Adapty.activate("YOUR_API_KEY") Adapty.delegate = self // Configure Firebase FirebaseApp.configure() // Add state change listener for Firebase Authentication Auth.auth().addStateDidChangeListener { (auth, user) in if let uid = user?.uid { // identify Adapty SDK with new Firebase user Adapty.identify(uid) { error in if let e = error { print("Sign in error: \(e.localizedDescription)") } else { print("User \(uid) signed in") } } } } return true } } extension AppDelegate: AdaptyDelegate { // MARK: - Adapty delegate func didReceiveUpdatedPurchaserInfo(_ purchaserInfo: PurchaserInfoModel) { // You can optionally post to the notification center whenever // purchaser info changes. // You can subscribe to this notification throughout your app // to refresh tableViews or change the UI based on the user's // subscription status NotificationCenter.default.post(name: NSNotification.Name(rawValue: "com.Adapty.PurchaserInfoUpdatedNotification"), object: purchaserInfo) } } ``` ```kotlin showLineNumbers class App : Application() { override fun onCreate() { super.onCreate() // Configure Adapty Adapty.activate(this, "YOUR_API_KEY") Adapty.setOnPurchaserInfoUpdatedListener(object : OnPurchaserInfoUpdatedListener { override fun onPurchaserInfoReceived(purchaserInfo: PurchaserInfoModel) { // handle any changes to subscription state } }) // Add state change listener for Firebase Authentication FirebaseAuth.getInstance().addAuthStateListener { auth -> val currentUserId = auth.currentUser?.uid if (currentUserId != null) { // identify Adapty SDK with new Firebase user Adapty.identify(currentUserId) { error -> if (error == null) { //success } } } else { Adapty.logout { } } } } } ``` --- # File: flutter-check-subscription-status.md --- --- title: "Check subscription status in Flutter SDK" description: "Learn how to check subscription status in your Flutter app with Adapty." displayed_sidebar: sdkflutter --- 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-deal-with-att.md --- --- title: "Deal with ATT in Flutter SDK" description: "Get started with Adapty on Flutter to streamline subscription setup and management." displayed_sidebar: sdkflutter --- 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: flutter-display-legacy-pb-paywalls.md --- --- title: "Display legacy Paywall Builder paywalls in Flutter SDK" description: "Learn how to display legacy Paywall Builder paywalls in your Flutter app with Adapty SDK." displayed_sidebar: sdkflutter --- This page contains guides for displaying legacy Paywall Builder paywalls in your Flutter app. Choose the topic you need: - **[Fetch legacy Paywall Builder paywalls](flutter-get-legacy-pb-paywalls)** - Retrieve legacy paywalls and their configuration - **[Present legacy Paywall Builder paywalls](flutter-present-paywalls-legacy)** - Display legacy paywalls to users - **[Handle legacy paywall events](flutter-handling-events-legacy)** - Manage legacy paywall interactions - **[Hide legacy Paywall Builder paywalls](flutter-hide-legacy-paywall-builder-paywalls)** - Hide legacy paywalls --- # File: flutter-get-legacy-pb-paywalls.md --- --- title: "Fetch legacy Paywall Builder paywalls in Flutter SDK" description: "Retrieve legacy PB paywalls in your Flutter app with Adapty SDK." displayed_sidebar: sdkflutter --- 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! 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 should hardcode is the placement ID. Response parameters: | Parameter | Description | | :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](flutter-sdk-models#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-flutter). For Flutter, the view configuration is automatically handled when you present the paywall using the `AdaptyUI.showPaywall()` method. --- # File: flutter-get-onboardings.md --- --- title: "Get onboardings in Flutter SDK" description: "Learn how to retrieve onboardings in Adapty for Flutter." displayed_sidebar: sdkflutter --- 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.

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 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`](flutter-sdk-models#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). ::: ```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.

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 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-get-pb-paywalls.md --- --- 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." displayed_sidebar: sdkflutter --- 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. 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-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`.

| 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 should hardcode is the placement ID. Response parameters: | Parameter | Description | | :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](flutter-sdk-models#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-flutter). ```dart showLineNumbers try { final view = await AdaptyUI().createPaywallView( paywall: paywall, ); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ## 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-handle-paywall-actions.md --- --- title: "Respond to button actions in Flutter SDK" description: "Handle paywall button actions in Flutter using Adapty for better app monetization." toc_max_heading_level: 4 --- 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 `close` action that dismisses the paywall. ```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-legacy.md --- --- 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-handling-events.md --- --- 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 #### 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" } } ```
#### Successful purchase If `Adapty.makePurchase()` succeeds, this method will be invoked: ```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. #### Failed purchase If `Adapty.makePurchase()` fails, this method will be invoked: ```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" } } } ```
#### Successful restore If `Adapty.restorePurchases()` 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 `Adapty.restorePurchases()` 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: ```javascript showLineNumbers title="Flutter" void paywallViewDidFailRendering(AdaptyUIPaywallView view, AdaptyError error) { } ```
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-handling-onboarding-events.md --- --- title: "Handle onboarding events in Flutter SDK" description: "Handle onboarding-related events in Flutter using Adapty." toc_max_heading_level: 4 --- 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, ) { _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: ```javascript showLineNumbers title="Flutter" // Full-screen presentation void onboardingViewOnPaywallAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { _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 } } ```
### Updating field state When your users respond to a quiz question or input their data into an input field, the state update event will be triggered: ```javascript showLineNumbers title="Flutter" // Full-screen presentation void onboardingViewOnStateUpdatedAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String elementId, AdaptyOnboardingsStateUpdatedParams params, ) { saveUserResponse(elementId, params.value); } // Embedded widget onStateUpdatedAction: (meta, elementId, params) { saveUserResponse(elementId, params.value); } ``` :::note If you want to save or process data, you need to implement the methods yourself. ::: The `params` object contains user input data, which can be one of the following types: - `select`: Single selection from a list of options - `multiSelect`: Multiple selections from a list of options - `input`: Text input from the user - `datePicker`: Date selected by the user
Saved data examples (Click to expand) ```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 } } } ```
### 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-hide-legacy-paywall-builder-paywalls.md --- --- title: "Hide legacy Paywall Builder paywalls in Flutter SDK" description: "Hide legacy paywalls in your Flutter app with Adapty SDK." displayed_sidebar: sdkflutter --- 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 } ``` --- # File: flutter-identifying-users.md --- --- title: "Identify users in Flutter SDK" description: "Identify users in Adapty to improve personalized subscription experiences." displayed_sidebar: sdkflutter --- 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. --- # File: flutter-implement-paywalls-manually.md --- --- title: "Implement paywalls manually in Flutter SDK" description: "Learn how to implement paywalls manually in your Flutter app with Adapty SDK." displayed_sidebar: sdkflutter --- This page contains guides for implementing paywalls manually in your Flutter app. Choose the topic you need: - **[Fetch paywalls and products](fetch-paywalls-and-products-flutter)** - Retrieve paywalls and product data - **[Present remote config paywalls](present-remote-config-paywalls-flutter)** - Display remote config paywalls - **[Accept purchases](flutter-making-purchases)** - Handle purchase transactions - **[Restore purchases](flutter-restore-purchase)** - Restore previous purchases - **[Implement Observer mode](implement-observer-mode-flutter)** - Set up Observer mode for analytics and paywall integration - **[Report transactions in Observer Mode](report-transactions-observer-mode-flutter)** - Report purchase transactions in Observer Mode - **[Troubleshooting](flutter-troubleshoot-purchases)** - Resolve common purchase issues --- # File: flutter-legacy-install.md --- --- title: "Legacy installation guide" description: "Get started with Adapty on Flutter to streamline subscription setup and management." displayed_sidebar: sdkflutter --- 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" ``` ## 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" ``` 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-legacy.md --- --- title: "Legacy Flutter SDK guides" description: "Legacy documentation for Adapty Flutter SDK." displayed_sidebar: sdkflutter --- This page contains legacy documentation for Adapty Flutter SDK. Choose the topic you need: - **[Legacy installation guide](flutter-legacy-install)** - Install and configure legacy Flutter SDK - **[Display legacy Paywall Builder paywalls](flutter-display-legacy-pb-paywalls)** - Work with legacy paywall builder --- # File: flutter-listen-subscription-changes.md --- --- 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](sdk-models#adaptyprofile) 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](sdk-models#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: ```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-localizations-and-locale-codes.md --- --- title: "Use localizations and locale codes in Flutter SDK" description: "Manage app localizations and locale codes to reach a global audience." displayed_sidebar: sdkflutter --- ## 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: ```dart showLineNumbers // 1. Modify your app_en.arb, app_es.arb, app_pt_br.arb files /* app_en.arb */ "adapty_paywalls_locale": "en", /* app_es.arb */ "adapty_paywalls_locale": "es", /* app_pt_br.arb */ "adapty_paywalls_locale": "pt-br", // 2. Extract and use the locale code final locale = AppLocalizations.of(context)!.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: ```dart showLineNumbers final locale = Localizations.localeOf(context).languageCode; // 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: flutter-making-purchases.md --- --- title: "Make purchases in mobile app in Flutter SDK" description: "Guide on handling in-app purchases and subscriptions using Adapty." displayed_sidebar: sdkflutter --- 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 In paywalls built with [Paywall Builder](adapty-paywall-builder) purchases are processed automatically with no additional code. If that's your case — you can skip this step. ::: This snippet is valid for v.2.0 or later. ```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`](sdk-models#adaptypaywallproduct) object retrieved from the paywall. | Response parameters: | Parameter | Description | |---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Profile** |

If the request has been successful, the response contains this object. An [AdaptyProfile](sdk-models#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 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 [`AdaptySubscriptionUpdateParameters`](sdk-models#adaptysubscriptionupdateparameters) 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}` ::: --- # File: flutter-migration-guide-310.md --- --- 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.md --- --- 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: flutter-onboardings.md --- --- title: "Onboardings in Flutter SDK" description: "Learn how to work with onboardings in your Flutter app with Adapty SDK." displayed_sidebar: sdkflutter --- This page contains all guides for working with onboardings in your Flutter app. Choose the topic you need: - **[Get onboardings](flutter-get-onboardings)** - Retrieve onboardings from Adapty - **[Display onboardings](flutter-present-onboardings)** - Present onboardings to users - **[Handle onboarding events](flutter-handling-onboarding-events)** - Manage onboarding interactions --- # File: flutter-paywalls.md --- --- title: "Paywalls in Flutter SDK" description: "Learn how to work with paywalls in your Flutter app with Adapty SDK." displayed_sidebar: sdkflutter --- This page contains all guides for working with paywalls in your Flutter app. Choose the topic you need: - **[Get paywalls](flutter-get-pb-paywalls)** - Retrieve paywalls from Adapty - **[Display paywalls](flutter-present-paywalls)** - Present paywalls to users - **[Handle paywall events](flutter-handling-events)** - Manage paywall interactions - **[Work with paywalls offline](flutter-use-fallback-paywalls)** - Use fallback paywalls when offline - **[Localize paywalls](flutter-localizations-and-locale-codes)** - Support multiple languages - **[Implement web paywalls](flutter-web-paywall)** - Use web-based paywalls - **[Implement paywalls manually](flutter-implement-paywalls-manually)** - Build custom paywall UI --- # File: flutter-present-onboardings.md --- --- title: "Present onboardings in Flutter SDK" description: "Learn how to present onboardings effectively to drive more conversions." displayed_sidebar: sdkflutter --- 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**: Modal presentation that can be dismissed by users through native platform gestures (swipe, back button). Best for optional onboardings where users should be able to skip or dismiss the content. - **Embedded widget (platform view)**: Embedded component gives you complete control over dismissal through your own UI and logic. Ideal for required onboardings where you want to ensure users complete the flow before proceeding. ## Present as standalone screen To display an onboarding as a standalone screen that users can dismiss, 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. ::: :::note This approach is best for optional onboardings where users should have the freedom to dismiss the screen using native gestures (swipe down on iOS, back button on Android). To have more customization options, [embed it in the component hierarchy](#embed-in-widget-hierarchy). ::: ```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 } ``` ## Embed in widget hierarchy To embed an onboarding within your existing widget tree, use the `AdaptyUIOnboardingPlatformView` widget directly in your Flutter widget hierarchy. This approach gives you full control over when and how the onboarding can be dismissed. :::note This approach is ideal for required onboardings, mandatory tutorials, or any flow where you need to ensure users complete the onboarding before proceeding. You can control dismissal through your own UI elements and logic. ::: ```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 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 override this in your Flutter app: - To customize the native loader on iOS, add `AdaptyOnboardingPlaceholderView.xib` to your Xcode project. - For full control, overlay your own widget above `AdaptyUIOnboardingPlatformView` and hide it on `onDidFinishLoading`. - To customize the native loader on Android, create `adapty_onboarding_placeholder_view.xml` in `res/layout` and define a placeholder there. This helps create seamless transitions and custom loading experiences. --- # File: flutter-present-paywalls-legacy.md --- --- 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-present-paywalls.md --- --- 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). ::: 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 } ``` :::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-quickstart-identify.md --- --- 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). ## 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, created anonymous profiles won't affect the dashboard [analytics](analytics-charts.md), because installs will be counted by new device IDs. However, if you want to change this behavior and count new customer user IDs instead of device IDs, go to **App settings** and set up [**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: flutter-quickstart-paywalls.md --- --- title: "Enable purchases by using paywalls in Flutter SDK" description: "Quickstart guide to setting up Adapty for in-app subscription management." displayed_sidebar: sdkflutter --- 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-making-purchases). | | 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. ## 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. 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-reference.md --- --- title: "Reference for Flutter SDK" description: "Reference documentation for Adapty Flutter SDK." displayed_sidebar: sdkflutter --- This page contains reference documentation for Adapty Flutter SDK. Choose the topic you need: - **[SDK models](flutter-sdk-models)** - Data models and structures used by the SDK - **[Handle errors](error-handling-on-flutter-react-native-unity)** - Error handling and troubleshooting - **[Flutter SDK reference](https://pub.dev/packages/adapty_flutter)** - Complete API documentation --- # File: flutter-restore-purchase.md --- --- 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`](sdk-models#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: flutter-sdk-migration-guides.md --- --- title: "Flutter SDK Migration Guides" description: "Migration guides for Adapty Flutter SDK versions." --- This page contains all migration guides for Adapty Flutter SDK. Choose the version you want to migrate to for detailed instructions: - **[Migrate to v. 3.8](flutter-migration-guide-38)** - **[Migrate to v. 3.4](migration-to-flutter-sdk-34)** - **[Migrate to v. 3.3](migration-to-flutter330)** - **[Migrate to v. 3.0](migration-to-flutter-sdk-v3)** --- # File: flutter-sdk-models.md --- --- title: "Flutter SDK Models" description: "Understand Adapty's SDK models to optimize in-app purchase handling." displayed_sidebar: sdkflutter --- ## Interfaces ### AdaptyOnboarding Information about an [onboarding](onboardings.md). | Name | Type | Description | |-------------------|---------------------------------------------------------------------|--------------------------------------------------------| | id | string | An identifier of an onboarding, configured in Adapty Dashboard | | placement | [AdaptyPlacement](#adaptyplacement) | A placement, configured in Adapty Dashboard | | hasViewConfiguration | boolean | If true, it is possible to fetch the view object and use it with AdaptyUI library | | name | string | Name of the onboarding flow | | remoteConfig | [AdaptyRemoteConfig](#adaptyremoteconfig) (optional) | A remote config configured in Adapty Dashboard for this onboarding | | variationId | string | An identifier of a variation, used to attribute purchases to this onboarding | ### AdaptyPaywallProduct An information about a [product.](https://swift.adapty.io/documentation/adapty/adaptypaywallproduct) | Name | Type | Description | |:-----------------------------------------|:------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | vendorProductId | string | Unique identifier of a product from App Store Connect or Google Play Console | | adaptyProductId | string | Unique identifier of the product in Adapty | | paywallVariationId | string | Same as variationId property of the parent AdaptyPaywall | | paywallABTestName | string | Same as abTestName property of the parent AdaptyPaywall | | paywallName | string | Same as name property of the parent AdaptyPaywall | | paywallProductIndex | number | The index of the product in the paywall | | localizedDescription | string | A description of the product | | localizedTitle | string | The name of the product | | price | [AdaptyPrice](#adaptyprice) (optional) | The cost of the product in the local currency | | subscription | [AdaptyProductSubscription](#adaptyproductsubscription) (optional) | Detailed information about subscription (intro, offers, etc.) | | ios | object (optional) | iOS-specific properties | | ios.isFamilyShareable | boolean | Boolean value that indicates whether the product is available for family sharing in App Store Connect. Will be false for iOS version below 14.0 and macOS version below 11.0. iOS Only. | | ios.regionCode | string (optional) | The region code of the locale used to format the price of the product. ISO 3166 ALPHA-2 (US, DE). iOS Only. | ### AdaptyPrice | Name | Type | Description | | :---------------- | :--------------- | :------------------------------------------------------------------------------------------------------------------------------- | | amount | number | Price as number | | currencyCode | string (optional) | The currency code of the locale used to format the price of the product. The ISO 4217 (USD, EUR) | | currencySymbol | string (optional) | The currency symbol of the locale used to format the price of the product. ($, €) | | localizedString | string (optional) | A price's language is determined by the preferred language set on the device. On Android, the formatted price from Google Play as is | ### AdaptyProductSubscription | Name | Type | Description | |:------------------------------|:------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | subscriptionPeriod | [AdaptyProductSubscriptionPeriod](#adaptyproductsubscriptionperiod) | The period details for products that are subscriptions. Will be null for iOS version below 11.2 and macOS version below 10.14.4. | | localizedSubscriptionPeriod | string (optional) | The period's language is determined by the preferred language set on the device | | offer | [AdaptySubscriptionOffer](#adaptysubscriptionoffer) (optional) | A subscription offer if available for the auto-renewable subscription | | ios | object (optional) | iOS-specific properties | | ios.groupIdentifier | string (optional) | An identifier of the subscription group to which the subscription belongs. Will be null for iOS version below 12.0 and macOS version below 10.14. iOS Only. | | android | object (optional) | Android-specific properties | | android.basePlanId | string | The identifier of the base plan. Android Only. | | android.renewalType | string (optional) | The renewal type. Possible values: 'prepaid', 'autorenewable'. Android Only. | ### AdaptyProductSubscriptionPeriod | Name | Type | Description | | :------------ | :--------------- | :------------------------------------------------------------------------------------------------------------------------------- | | numberOfUnits | number | A number of period units | | unit | ProductPeriod | A unit of time that a subscription period is specified in. The possible values are: `day`, `week`, `month`, `year` | ### AdaptySubscriptionOffer | Name | Type | Description | |:------------------------------|:------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | identifier | string | Unique identifier of a discount offer for a product | | phases | array of [AdaptySubscriptionPhase](#adaptysubscriptionphase) | A list of discount phases available for this offer | ### AdaptySubscriptionPhase | Name | Type | Description | |:------------------------------|:------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | localizedNumberOfPeriods | string (optional) | A formatted number of periods of a discount for a user's locale | | localizedSubscriptionPeriod | string (optional) | A formatted subscription period of a discount for a user's locale | | numberOfPeriods | number | A number of periods this product discount is available | | price | [AdaptyPrice](#adaptyprice) | Discount price of a product in a local currency | | subscriptionPeriod | [AdaptyProductSubscriptionPeriod](#adaptyproductsubscriptionperiod) | An information about period for a product discount | | paymentMode | OfferType | A payment mode for this product discount. Possible values: `free_trial`, `pay_as_you_go`, `pay_up_front` | ### AdaptyPaywall An information about a [paywall.](https://swift.adapty.io/documentation/adapty/adaptypaywall) | Name | Type | Description | |--------------------|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | placement | [AdaptyPlacement](#adaptyplacement) | A placement, configured in Adapty Dashboard | | hasViewConfiguration | boolean | If true, it is possible to fetch the view object and use it with AdaptyUI library | | name | string | A paywall name | | remoteConfig | [AdaptyRemoteConfig](#adaptyremoteconfig) (optional) | A remote config configured in Adapty Dashboard for this paywall | | variationId | string | An identifier of a variation, used to attribute purchases to this paywall | | instanceIdentity | string | Unique identifier of the paywall configuration | ### AdaptyPlacement | Name | Type | Description | | :------------ | :--------------- | :------------------------------------------------------------------------------------------------------------------------------- | | abTestName | string | Parent A/B test name | | audienceName | string | A name of an audience to which the paywall belongs | | id | string | ID of a placement configured in Adapty Dashboard | | revision | number | Current revision (version) of a paywall. Every change within a paywall creates a new revision | | isTrackingPurchases | boolean (optional) | Whether the placement is tracking purchases | | audienceVersionId | string | Version ID of the audience | ### AdaptyRemoteConfig | Name | Type | Description | | :------------ | :--------------- | :------------------------------------------------------------------------------------------------------------------------------- | | lang | string | Identifier of a paywall locale | | data | object | A custom dictionary configured in Adapty Dashboard for this paywall | | dataString | string | A custom JSON string configured in Adapty Dashboard for this paywall | ### AdaptyProfile An information about a [user's](https://swift.adapty.io/documentation/adapty/adaptyprofile) subscription status and purchase history. | Name | Type | Description | | :--------------- | :---------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------- | | profileId | string | An identifier of the user in Adapty | | customerUserId | string (optional) | An identifier of the user in your system | | customAttributes | object | Previously set user custom attributes with the updateProfile method | | accessLevels | object\ | The keys are access level identifiers configured by you in Adapty Dashboard. The values are AccessLevel objects. Can be null if the customer has no access levels | | subscriptions | object\ | The keys are product ids from App Store Connect. The values are Subscription objects. Can be null if the customer has no subscriptions | | nonSubscriptions | object\ | The keys are product ids from App Store Connect. The values are arrays of NonSubscription objects. Can be null if the customer has no purchases | ### AdaptyProfile.AccessLevel Information about the [user's access level.](https://swift.adapty.io/documentation/adapty/adaptyprofile/accesslevel) | Name | Type | Description | |----|----|-----------| | id | string | Unique identifier of the access level configured by you in Adapty Dashboard | | isActive | boolean | Whether the access level is active. Generally, you have to check just this property to determine if the user has access to premium features | | vendorProductId | string | The identifier of the product in the App Store Connect that unlocked this access level | | store | string | The store of the purchase that unlocked this access level. The possible values are: app_store, play_store, adapty | | activatedAt | DateTime | The time when the access level was activated | | renewedAt | DateTime (optional) | The time when the access level was renewed | | expiresAt | DateTime (optional) | The time when the access level will expire (could be in the past and could be null for lifetime access) | | isLifetime | boolean | Whether the access level is active for a lifetime (no expiration date). If set to true you shouldn't check expires_at, or you could just check isActive | | activeIntroductoryOfferType | string (optional) | The type of active introductory offer. Possible values are: free_trial, pay_as_you_go, pay_up_front. If the value is not null, it means that the offer was applied during the current subscription period | | activePromotionalOfferType | string (optional) | The type of active promotional offer. Possible values are: free_trial, pay_as_you_go, pay_up_front. If the value is not null, it means that the offer was applied during the current subscription period | | activePromotionalOfferId | string (optional) | An identifier of active promotional offer | | offerId | string (optional) | Offer identifier | | willRenew | boolean | Whether the auto-renewable subscription is set to renew | | isInGracePeriod | boolean | Whether the auto-renewable subscription is in the grace period | | unsubscribedAt | DateTime (optional) | The time when the auto-renewable subscription was cancelled. Subscription can still be active, it just means that auto-renewal turned off. Will be set to null if the user reactivates the subscription | | billingIssueDetectedAt | DateTime (optional) | The time when billing issue was detected. Subscription can still be active. Will be set to null if the charge will be made | | startsAt | DateTime (optional) | The time when this access level has started (could be in the future) | | cancellationReason | string (optional) | The reason why the subscription was cancelled. Possible values are: voluntarily_cancelled, billing_error, refund, price_increase, product_was_not_available, unknown | | isRefund | boolean | Whether the purchase was refunded | ### AdaptyProfile.Subscription Information about the [user's subscription.](https://swift.adapty.io/documentation/adapty/adaptyprofile/subscription) | Name | Type | Description | |----|----|-----------| | store | string | The store of the purchase. The possible values are: app_store, play_store, adapty | | vendorProductId | string | The identifier of the product in the App Store Connect | | vendorTransactionId | string | Transaction id from the App Store | | vendorOriginalTransactionId | string | Original transaction id from the App Store. For auto-renewable subscription, this will be the id of the first transaction in the subscription | | isActive | boolean | Whether the subscription is active | | isLifetime | boolean | Whether the subscription is active for a lifetime (no expiration date). If set to true you shouldn't check expires_at, or you could just check isActive | | activatedAt | DateTime | The time when the subscription was activated | | renewedAt | DateTime (optional) | The time when the subscription was renewed | | expiresAt | DateTime (optional) | The time when the subscription will expire (could be in the past and could be null for lifetime access) | | startsAt | DateTime (optional) | The time when the subscription has started (could be in the future) | | unsubscribedAt | DateTime (optional) | The time when the auto-renewable subscription was cancelled. Subscription can still be active, it just means that auto-renewal turned off. Will be set to null if a user reactivates the subscription | | billingIssueDetectedAt | DateTime (optional) | The time when billing issue was detected (Apple was not able to charge the card). Subscription can still be active. Will be set to null if the charge will be made | | isInGracePeriod | boolean | Whether the auto-renewable subscription is in the grace period | | isSandbox | boolean | Whether the product was purchased in the sandbox environment | | isRefund | boolean | Whether the purchase was refunded | | willRenew | boolean | Whether the auto-renewable subscription is set to renew | | activeIntroductoryOfferType | string (optional) | The type of active introductory offer. Possible values are: free_trial, pay_as_you_go, pay_up_front. If the value is not null, it means that the offer was applied during the current subscription period | | activePromotionalOfferType | string (optional) | The type of active promotional offer. Possible values are: free_trial, pay_as_you_go, pay_up_front. If the value is not null, it means that the offer was applied during the current subscription period | | activePromotionalOfferId | string (optional) | An identifier of active promotional offer | | offerId | string (optional) | Offer identifier | | cancellationReason | string (optional) | The reason why the subscription was cancelled. Possible values are: voluntarily_cancelled, billing_error, refund, price_increase, product_was_not_available, unknown | ### AdaptyProfile.NonSubscription Information about the user's non-subscription purchases. | Name | Type | Description | |----|----|-----------| | purchaseId | string | The identifier of the purchase in Adapty. You can use it to ensure that you've already processed this purchase (for example tracking one time products) | | store | string | The store of the purchase. The possible values are: app_store, play_store, adapty | | vendorProductId | string | The identifier of the product in the App Store Connect | | vendorTransactionId | string (optional) | Transaction id from the App Store | | purchasedAt | DateTime | The time when the product was purchased | | isSandbox | boolean | Whether the product was purchased in the sandbox environment | | isRefund | boolean | Whether the purchase was refunded | | isConsumable | boolean | Whether the product should only be processed once. If true, the purchase will be returned by Adapty API one time only | ### AdaptyAndroidSubscriptionUpdateParameters Parameters to change one subscription to another. | Name | Type | Description | | :-------------------- | :----- | :----------------------------------------------------------- | | oldSubVendorProductId | string | The product id for current subscription to change | | replacementMode | [AdaptyAndroidSubscriptionUpdateReplacementMode](#adaptyandroidsubscriptionupdatereplacementmode) | The proration mode for subscription update | ### Enums #### ProductPeriod - `day` - Day period - `week` - Week period - `month` - Month period - `year` - Year period #### OfferType - `free_trial` - Free trial - `pay_as_you_go` - Pay as you go - `pay_up_front` - Pay up front #### AdaptyAndroidSubscriptionUpdateReplacementMode - `immediate_with_time_proration` - Immediate with time proration - `immediate_and_charge_prorated_price` - Immediate and charge prorated price - `immediate_without_proration` - Immediate without proration - `deferred` - Deferred - `immediate_and_charge_full_price` - Immediate and charge full price --- # File: flutter-sdk-overview.md --- --- title: "Flutter SDK overview" description: "Learn about Adapty Flutter SDK and its key features." slug: /flutter-sdk-overview displayed_sidebar: sdkflutter --- [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-Flutter.svg?style=flat&logo=flutter)](https://github.com/adaptyteam/AdaptySDK-Flutter/releases) Welcome! We're here to make in-app purchases a breeze 🚀 We've built the Adapty Flutter 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 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-flutter.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-flutter.md): Add the SDK as a dependency to your project and activate it in the code. 2. [Enable purchases through paywalls](flutter-quickstart-paywalls.md): Set up the purchase flow so users can buy products. 3. [Check the subscription status](flutter-check-subscription-status.md): Automatically check the user's subscription state and control their access to paid content. 4. [Identify users (optional)](flutter-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-Flutter/tree/master/example) that demonstrates the full setup ## 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: flutter-setting-user-attributes.md --- --- title: "Set user attributes in Flutter SDK" description: "Learn how to set user attributes in Adapty to enable better audience segmentation." displayed_sidebar: sdkflutter --- 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 up to 30 characters | | 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-test.md --- --- title: "Test & release in Flutter SDK" description: "Learn how to check subscription status in your Flutter app with Adapty." displayed_sidebar: sdkflutter --- 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. For comprehensive testing of your in-app purchases, see our platform-specific testing guides: [iOS testing guide](testing-purchases-ios.md) and [Android testing guide](testing-on-android.md). --- # File: flutter-troubleshoot-paywall-builder.md --- --- 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-troubleshoot-purchases.md --- --- 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-use-fallback-paywalls.md --- --- title: "Flutter - Use fallback paywalls" description: "Implement fallback paywalls in Flutter with Adapty to ensure seamless subscription handling." --- To use fallback paywalls, call the `.setFallback` method. Pass the path to the fallback JSON file you [downloaded in the Adapty Dashboard](fallback-paywalls#download-fallback-paywalls-as-a-file-in-the-adapty-dashboard). Place this method in your code **before** fetching a paywall, ensuring that the mobile app possesses it when a fallback paywall is required to replace the standard one. ```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** | The path to the fallback JSON file you [downloaded in the Adapty Dashboard](fallback-paywalls#download-fallback-paywalls-as-a-file-in-the-adapty-dashboard). | :::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-user.md --- --- title: "Users & access in Flutter SDK" description: "Learn how to work with users and access levels in your Flutter app with Adapty SDK." displayed_sidebar: sdkflutter --- This page contains all guides for working with users and access levels in your Flutter app. Choose the topic you need: - **[Identify users](flutter-identifying-users)** - Learn how to identify users in your app - **[Update user data](flutter-setting-user-attributes)** - Set user attributes and profile data - **[Listen for subscription status changes](flutter-listen-subscription-changes)** - Monitor subscription changes in real-time - **[Deal with App Tracking Transparency (ATT)](flutter-deal-with-att)** - Handle ATT requirements - **[Kids Mode](kids-mode-flutter)** - Implement Kids Mode for your app --- # File: flutter-web-paywall.md --- --- title: "Implement web paywalls in Flutter SDK" description: "Set up a web paywall to get paid without the App Store fees and audits." displayed_sidebar: sdkflutter --- :::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. ```swift 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 | --- # File: flutterflow.md --- --- title: "Adapty Plugin for FlutterFlow" description: "Integrate FlutterFlow with Adapty for enhanced subscription management." --- Adapty Plugin Banner :::tip [Start using Adapty with FlutterFlow and get 3 months of Pro+ for free!](https://app.adapty.io/flutterflow-offer/.) ::: Adapty is a versatile platform designed to help mobile apps grow. Whether you’re just starting out or already have thousands of users, Adapty lets you save months on integrating in-app purchases and double subscription revenue with paywall management. The Adapty plugin for FlutterFlow lets you leverage all of Adapty’s features without any coding. You can design paywall pages in FlutterFlow, enable purchases for them, and then remotely control which products get displayed on them, including targeting to specific user groups or A/B testing. And after you release your app, you can instantly access detailed analytics of your customers' purchases right in our dashboard. Want to update the products available on your paywall? It’s simple! Make changes in just a few clicks within the Adapty Dashboard, and your customers will see the new products immediately — no need to release a new app version! What else Adapty offers you: - **Subscriptions and in-App Purchases**: Adapty handles server-side receipt validation for you and syncs your customers across all platforms, including the web. - **A/B Testing for paywalls**: Test different prices, durations, trial periods, and visual elements to optimize your subscription and one-time offerings. - **Powerful analytics**: Access detailed metrics to better understand and improve your app’s monetization. - **Integrations**: Adapty seamlessly connects with third-party analytics tools like Amplitude, AppsFlyer, Adjust, Branch, Mixpanel, Facebook Ads, AppMetrica, custom Webhooks, and more. :::tip [Start using Adapty with FlutterFlow and get 3 months of Pro+ for free!](https://app.adapty.io/flutterflow-offer/.) ::: --- # File: forwarding-events-to-adapty.md --- --- title: "Forwarding Events to Adapty" description: "" --- Apple and Google provide a way to send subscription event updates the moment they occur on their servers. It's essential to send them to Adapty to avoid delays in processing subscription status and therefore, providing the best experience for your customers. In most cases, you just have to [enable App Store server notifications](enable-app-store-server-notifications) or [enable Real-time developer notifications (RTDN) in Google Play Console](enable-real-time-developer-notifications-rtdn); no coding is needed on your side. However, if you process these events and want to keep doing it, make sure to forward events to Adapty from your backend. It's straightforward; you just have to send raw \(without any modification\) payload from Apple or Google as a request body to our server using the same URL from settings. Here are examples for different programming languages: ```python showLineNumbers url = "https://api.adapty.io/api/v1/sdk/apple/webhook/123a258e62fad41bfa734f4b0dbcad456/" # don't forget to replace this URL payload = '{"latest_receipt":"abc=","notification_type":"INITIAL_BUY",...}' # json encoded payload from Apple/Google headers = { 'Content-Type': 'application/json' } response = requests.request("POST", url, headers=headers, data=payload) ``` ```javascript showLineNumbers const axios = require('axios'); const url = 'https://api.adapty.io/api/v1/sdk/apple/webhook/123a258e62fad41bfa734f4b0dbcad456/'; // don't forget to replace this URL const payload = '{"latest_receipt":"abc=","notification_type":"INITIAL_BUY",...}'; // json encoded payload from Apple/Google const config = { method: 'post', url: url, headers: { 'Content-Type': 'application/json' }, data: payload, }; const response = await axios(config); ``` ```php showLineNumbers setUrl($url); $request->setMethod(HTTP_Request2::METHOD_POST); $request->setConfig(array( 'follow_redirects' => TRUE )); $request->setHeader(array( 'Content-Type' => 'application/json' )); $request->setBody($payload); $response = $request->send(); ``` ```ruby showLineNumbers require "uri" require "net/http" url = URI("https://api.adapty.io/api/v1/sdk/apple/webhook/123a258e62fad41bfa734f4b0dbcad456/") # don't forget to replace this URL payload = '{"latest_receipt":"abc=","notification_type":"INITIAL_BUY",...}' # json encoded payload from Apple/Google https = Net::HTTP.new(url.host, url.port) https.use_ssl = true request = Net::HTTP::Post.new(url) request["Content-Type"] = "application/json" request.body = payload response = https.request(request) ``` ```java showLineNumbers String url = "https://api.adapty.io/api/v1/sdk/apple/webhook/123a258e62fad41bfa734f4b0dbcad456/"; // don't forget to replace this URL String payload = "{\"latest_receipt\":\"abc=\",\"notification_type\":\"INITIAL_BUY\",...}" // json encoded payload from Apple/Google OkHttpClient client = new OkHttpClient().newBuilder() .build(); MediaType mediaType = MediaType.parse("application/json"); RequestBody body = RequestBody.create(mediaType, payload); Request request = new Request.Builder() .url(url) .method("POST", body) .addHeader("Content-Type", "application/json") .build(); Response response = client.newCall(request).execute(); ``` ```go showLineNumbers package main "fmt" "strings" "net/http" "io" ) func main() { url := "https://api.adapty.io/api/v1/sdk/apple/webhook/123a258e62fad41bfa734f4b0dbcad456/" // Replace this with the actual URL payload := strings.NewReader(`{"latest_receipt":"abc=","notification_type":"INITIAL_BUY"}`) // Ensure valid JSON method := "POST" client := &http.Client{} req, err := http.NewRequest(method, url, payload) if err != nil { fmt.Println("Error creating request:", err) return } req.Header.Add("Content-Type", "application/json") res, err := client.Do(req) if err != nil { fmt.Println("Error sending request:", err) return } defer res.Body.Close() } ``` --- # File: general.md --- --- title: "App settings" description: "Explore general settings and configurations in Adapty for seamless use." --- You can navigate to the General tab of the App Settings page to manage your app's behavior, appearance, and revenue sharing. Here, you can customize your app's name and icon, manage your Adapty SDK and API keys, set your Small Business Program status, and choose the timezone for your app's analytics and charts. ## 1. App details Choose a unique name and icon that represent your app in the Adapty interface. Please note that the app name and icon will not affect the app's name and icon in the App Store or Google Play. Also, make sure to select an appropriate App Category that accurately reflects your app's purpose and content. This will help users discover your app and ensure it appears in the appropriate app store categories. ## 2\. Member of Small Business Program and Reduced Service Fee If you're a member of the Apple Small Business Program and/or Google's Reduced Service Fee program, you can let Adapty know by specifying the period that you are a member. Adapty will adjust the commission rate accordingly, so you can keep more of your revenue. Please note that this setting applies only to future transactions, and you need to update it if your Small Business Program status changes. You can learn more about the [App Store Small Business Program](app-store-small-business-program) and [Google's Reduced Service Fee](google-reduced-service-fee). ## 3\. Reporting timezone Choose the timezone that corresponds to the location where you're based, or where your app's analytics and charts are most relevant. We recommend using the same timezone as your App Store Connect or Google Play Console account to ensure consistency. Please note that this timezone setting does not affect third-party integrations in the Adapty system, which use the UTC timezone. You can access the timezone settings in the Reported timezone section of the General Tab on the App Settings page. You can also choose to set the same timezone for all the apps in your Adapty account by checking the corresponding box. ## 4\. Installs definition for analytics Choose what is defined as a new install event in analytics: | Base | Description | | ---------------------- | ------------------------------------------------------------ | | New device_ids |

(Recommended) Any new instance (installation) of the app counts as a new install event. This includes both the first installs and reinstalls. If a user has multiple devices, each installation on a different device is counted (if a user has your app on 5 devices, you'll see 5 installs).

| | New customer_user_ids |

This option only makes sense if you're [iOS](identifying-users), [Android](android-identifying-users), [Flutter](flutter-identifying-users), [React Native](react-native-identifying-users), and [Unity](unity-identifying-users). In that case for logged in users we only count the first installation of the app. If a user installs the app on more devices, they won't be counted as new installs. Anonymous users (those that have not logged in) are not counted in analytics.

Reinstallations or logging in on any user's device aren't counted as new installs.

Note that if you are not [iOS](identifying-users), [Android](android-identifying-users), [Flutter](flutter-identifying-users), [React Native](react-native-identifying-users), and [Unity](unity-identifying-users), you won't get any installs in analytics with this option enabled.

| | New profiles in Adapty | (Legacy) Every app installation, reinstallation and anonymous profiles created during logouts are counted as new installs. | Keep in mind that this option only affects the [**Analytics**](https://app.adapty.io/analytics) page and does not impact the [**Overview**](https://app.adapty.io/overview) page, where you can configure the view separately. ## 5. App Store price increase logic To maintain accurate data and avoid discrepancies between Adapty analytics and App Store Connect results, it is important to select the appropriate option when adjusting configurations related to price increases in App Store Connect. So you can choose the logic that will be applied to subscription price increases in Adapty: - **Subscription price for existing users is preserved:** By selecting this option, the current price will be retained for your existing subscribers, even if you make changes to the price in the App Store Connect. This means that existing subscribers will continue to be billed at their original subscription price. - **When the subscription price is changed in App Store Connect, it changes for existing subscribers:** If you choose this option, any price changes made in the App Store Connect will be applied to your existing subscribers as well. This means that existing subscribers will be charged the new price reflecting the updated pricing set in the App Store Connect. :::warning It is important to consider that the selected option not only affects analytics in Adapty but also impacts integrations and overall transaction handling behavior. ::: Please ensure that you select the designated option that aligns with your desired approach to handling subscription prices for existing subscribers. This will help maintain accurate data and synchronization between Adapty analytics and the results obtained from the App Store Connect. ## 6. Sharing purchases between user accounts When a [iOS](identifying-users#setting-customer-user-id-on-configuration), [Android](android-identifying-users#setting-customer-user-id-on-configuration), [Flutter](flutter-identifying-users#setting-customer-user-id-on-configuration), [React Native](react-native-identifying-users#setting-customer-user-id-on-configuration), and [Unity](unity-identifying-users#setting-customer-user-id-on-configuration) tries to restore transactions or extend a subscription that is already associated with a different identified [iOS](identifying-users#setting-customer-user-id-on-configuration), [Android](android-identifying-users#setting-customer-user-id-on-configuration), [Flutter](flutter-identifying-users#setting-customer-user-id-on-configuration), [React Native](react-native-identifying-users#setting-customer-user-id-on-configuration), and [Unity](unity-identifying-users#setting-customer-user-id-on-configuration), you can control how Adapty responds by adjusting the **Sharing paid access between user accounts** dropdown: ## 7. SDK and API keys Use a Public SDK key to integrate Adapty SDKs into your app, and a Secret Key to access Adapty's Server API. You can generate new keys or revoke existing ones as needed. ## 8. Test devices Specify the devices to be used for testing to ensure they get instant updates for paywall or placement changes, bypassing any caching delays. For more information, see [Testing devices](test-devices). ## 9. Delete the app If you no longer need an app, you can delete it from Adapty. :::warning Please be aware that this action is irreversible, and you won't be able to restore the app or its data. ::: --- # File: generate-in-app-purchase-key.md --- --- title: "Generate In-App Purchase Key in App Store Connect" description: "Generate an in-app purchase key for secure transactions." --- The **In-App Purchase Key** is a specialized API key created within App Store Connect to validate the purchases by confirming their authenticity. :::note To generate API keys for the App Store Server API, you must hold either an Admin role or an Account Holder role in App Store Connect. You can also read about how to generate API Keys in the [Apple Developer Documentation](https://developer.apple.com/documentation/appstoreserverapi/creating_api_keys_to_use_with_the_app_store_server_api). ::: 1. Open **App Store Connect**. Proceed to [**Users and Access** → **Integrations** → **In-App Purchase**](https://appstoreconnect.apple.com/access/integrations/api/subs) section. 2. Then click the add button **(+)** next to the **Active** title. 3. In the opened **Generate In-App Purchase Key** window, enter the name of the key for your future reference. It will not be used in Adapty. 4. Click the **Generate** button. Once the **Generate in-App Purchase Key** window closes, you'll see the created key in the **Active** list. 5. Once you've generated your API key, click the **Download In-App Purchase Key** button to obtain the key as a file. 6. In the **Download in-App Purchase Key** window, click the **Download** button. The file is saved to your computer. It's crucial to keep this file secure for future uploading to the Adapty Dashboard. Note that the generated file can only be downloaded once, so ensure safe storage until you upload it. The generated .p8 key from the **In-App Purchase section** will be used when [configuring the initial integration of Adapty with the App Store](app-store-connection-configuration#step-3-upload-in-app-purchase-key-file). **What's next:** - [Configure App Store integration](app-store-connection-configuration) --- # File: get-onboardings.md --- --- title: "Get onboardings in iOS SDK" description: "Learn how to retrieve onboardings in Adapty for iOS." --- 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 iOS 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 SDK](sdk-installation-ios.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`](sdk-models#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: get-paid-in-onboardings.md --- --- title: "Connect paywalls to onboardings" --- You can set up a seamless transition from onboardings to paywalls, so that onboardings not only improve the user experience and retention but also generate revenue for you. There are two ways to connect paywalls to onboardings: - **Showing a paywall after onboarding**: Implement opening a paywall after onboarding is closed. - **Showing a paywall inside onboarding**: Trigger opening a paywall on tapping a button. Before you start, create a [paywall](paywalls.md) and [onboarding](onboardings.md) and add them to placements. :::important You need two different placements: one for a paywall and another for an onboarding. Ensure you use proper placement IDs when getting onboarding and paywall in your code. ::: ## Show paywall after onboarding To show a paywall after onboarding, you only need to handle an event generated each time users close the onboarding. As soon as users close onboarding, the [event](ios-handling-onboarding-events#closing-onboarding) is triggered. So, if you want to display a paywall after your onboarding immediately, you can implement [iOS](ios-present-paywalls.md), [Android](android-present-paywalls.md), [Flutter](flutter-present-paywalls.md), [React Native](react-native-present-paywalls.md), and [Unity](unity-present-paywalls.md) as a reaction to the event. ## Show paywall inside onboarding 1. In the onboarding builder, create a button that will redirect users to the paywall. Select **Open paywall** as its action. 2. You can assign any action ID to the button and use it to identify the paywall you need to open. However, the easiest way to open paywalls from onboardings is to make the action ID equal to a placement ID. This way, you can get and display paywalls right away without hardcoding placement IDs. To do this, go to the Adapty dashboard and find the paywall's placement ID. In the builder, paste the paywall placement ID in the ID field. 3. Now, when you have this button, each time your users tap it, it will generate an action containing the action ID. To handle this action in your app code, you will need to [get the paywall](fetch-paywalls-and-products.md) and then [display it](ios-quickstart-paywalls.md). --- # File: get-pb-paywalls.md --- --- 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. 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) 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.

| 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 should hardcode is the placement ID. Response parameters: | Parameter | Description | | :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](sdk-models#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 you have successfully loaded the paywall and its view configuration, you can present it in your mobile app. ## 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: getting-started-with-server-side-api-legacy.md --- --- title: "Getting started with legacy server-side API" description: "" --- :::warning **You are viewing the guide for the legacy server-side API.** For the latest version, refer to the [Server-side API V2](ss-authorization) and the [Migration Guide to Server-side API V2](migration-guide-to-server-side-API-v2). ::: With API you can: 1. Get user's subscription status. 2. Activate a subscription for a user with an [access level](access-level). 3. Get user's attributes. 4. Set user's attributes. :::note You can't get subscription events via API, but you can use [Webhook](webhook) or direct integration with a service that you're using. ::: To correctly work with API you need to use a unique ID for your users. This may be an email, a phone number, your internal ID. Without such an ID it's impossible to identify the same user on multiple platforms. ## Case 1: Syncing subscribers between web and mobile Whenever Web payment providers you use such as Stripe, ChargeBee, or any other, you can sync subscribers. For that: 1. _Use a unique ID for your users_. For example, email or phone number. 2. Check subscription status via API. 3. If a user is freemium, show him a paywall on the Web. 4. After successful payment, update subscription status in Adapty via API. 5. Your subscribers will be automatically in sync with mobile. ## Case 2: Grant a subscription :::note Due to security reasons, you can't grant a subscription via mobile SDK. ::: Imagine a case, when you run a promotional campaign with offers 7 days of a trial and you want to sync in with mobile experience. To do that: 1. Get a unique ID for a user. 2. Set premium access via paid access level with API with a duration of 7 days. After 7 days users who won't subscribe will be downgraded to the free tier. ## Case 3: Syncing users' attributes and custom properties You may have custom attributes for your users, other than defaults such as IDFA, device model, etc. For example, in a language learning service, you may want to save the number of words a student has learned. To do that: 1. Get a unique ID for a user. 2. Update attribute with API or SDK. With such attributes you can, for example, create a segment and run an A/B test. To learn more about legacy S2S API go to [API Specs](server-side-api-specs-legacy). --- # File: getting-started-with-server-side-api.md --- --- 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), [Flutter](flutter-identifying-users), [React Native](react-native-identifying-users), and [Unity](unity-identifying-users). 2. [Check their subscription status](ss-get-profile) 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](ss-set-transaction) 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), [Flutter](flutter-identifying-users), [React Native](react-native-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](ss-set-transaction) 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), [Flutter](flutter-identifying-users), [React Native](react-native-identifying-users), and [Unity](unity-identifying-users). 2. Use the API to [grant premium access](ss-grant-access-level) 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), [Flutter](flutter-identifying-users), [React Native](react-native-identifying-users), and [Unity](unity-identifying-users). 2. [Update the attribute](ss-update-profile) 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](ss-update-paywall.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](ss-get-profile) - [Create profile](ss-create-profile) - [Update profile](ss-update-profile) - [Delete profile](ss-delete-profile) - [Grant access level](ss-grant-access-level) - [Revoke access level](ss-revoke-access-level) - [Set transaction](ss-set-transaction) - [Validate purchase, provide access level to customer, and import their transaction history](ss-purchase-in-stripe) - [Add integration identifiers](ss-add-integration) - [Get paywall](ss-get-paywall) - [List paywalls](ss-list-paywalls) - [Update paywall](ss-update-paywall) --- # File: give-access-level-to-specific-customer.md --- --- title: "Give access level to specific customer" description: "Assign specific access levels to customers using Adapty’s advanced tools." --- You can manually adjust the access level for a particular customer right in the Adapty Dashboard. This is useful, especially in support scenarios. For example, if you'd like to extend a user's premium usage by an extra week as a thank-you for leaving a fantastic review. ## Give access level to a specific customer in the Adapty Dashboard 1. Open the **[Profiles and Segments](https://app.adapty.io/placements)** section from the Adapty main menu, then select the **Profiles** tab. 2. In the **Profiles** window, click on the customer you want to grant access to. 3. In the opened window, click the **Add access level** button. 4. In the opened **Add Access level** window, select the Access level to grant and when it should expire for this customer. 5. Click the **Apply** button. ## Give access level to a specific customer via API You also have the option to give a customer an access level from your server using the Adapty API. This comes in handy if you have bonuses for referrals or other events related to your products. Find additional details on the [Grant access level with server-side API](ss-grant-access-level) page. --- # File: google-cloud-storage.md --- --- title: "Google Cloud Storage" description: "Integrate Google Cloud Storage with Adapty for secure data storage." --- Adapty's integration with Google Cloud Storage allows you to store event and paywall visit data securely in one central location. You will be able to save your [subscription events](events) to your Google Cloud Storage bucket as .csv files. To set up this integration, you will need to follow a few simple steps in the Google Cloud Console and Adapty Dashboard. :::note Schedule Adapty sends your data to Google Cloud Storage every 24h at 4:00 UTC. Each file will contain data for the events created for the entire previous calendar day in UTC. For example, the data exported automatically at 4:00 UTC on March 8th will contain all the events created on March 7th from 00:00:00 to 23:59:59 in UTC. ::: ## How to set up Google Cloud storage integration To integrate Google Cloud Storage go to [**Integrations** -> **Google Cloud Storage**](https://app.adapty.io/integrations/google-cloud-storage), turn on a toggle from off to on, and fill out fields. First of all set credentials to build a connection between Google Cloud Storage and Adapty profiles. In the Adapty Dashboard, the following fields are needed to set up the connection: | Field | Description | | :---------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Google Cloud Client ID** | A unique identifier assigned to your Google Cloud project when you create a new client in the Google Cloud Console. Find this ID in the downloaded private [JSON key file](google-cloud-storage#create-google-cloud-storage-credentials) under the `client_id` field. | | **Google Cloud Project ID** | A user-assigned identifier for your Google Cloud project. Find this ID in the downloaded private JSON key file under the `project_id` field. | | **Google Cloud Service Account Private Key ID** | A unique identifier assigned to your private key when you create a new service account in the Google Cloud Console. Find this ID in the downloaded private JSON key file under the `private_key_id` field. | | **Google Cloud Bucket Name** | The name of the bucket in Google Cloud Storage where you want to store your data. It should be unique within the Google Cloud Storage environment and should not contain any spaces. | | **Email** | The email address associated with your service account in Google Cloud Console. It is used to grant access to resources in your project. | | **Folder inside the bucket** | The name of the folder inside the bucket where you want to store your data. It should be unique within the bucket and can be used to organize your data. This field is optional to fill. | ## Create Google Cloud Storage credentials This guide will help you create the necessary credentials in your Google Cloud Platform Console. In order for Adapty to upload raw data reports to your designated bucket, the service account's key is required, as well as write access to the corresponding bucket. By providing the service account's key and granting write access to the bucket, you allow Adapty to securely and efficiently transfer the raw data reports from its platform to your storage environment. :::warning Please note that we only support Service Account HMAC key authorization, means it's essential to ensure that your Service Account HMAC key has the "Storage Object Viewer", "Storage Legacy Bucket Writer" and "Storage Object Creator" roles added to it to enable proper access to Google Cloud Storage. ::: 1. For the first step, you need to go to the [IAM](https://console.cloud.google.com/projectselector2/iam-admin/serviceaccounts) section of your Google Cloud account and choose the relevant project or create a new one. 2. Next, create a new service account for the Adapty by clicking on the "+ CREATE SERVICE ACCOUNT" button. 3. Fill out the fields in the first step, as access will be granted at a later stage. In order to read more details about this page read the documentation [here](https://cloud.google.com/iam/docs/service-accounts-create). 4. To create and download a [private JSON key](https://cloud.google.com/iam/docs/keys-create-delete), navigate to the KEYS section and click on the "ADD KEY" button. 5. In the DETAILS section, locate the Email value linked to the recently created service account and make a copy of it. This information will be necessary for the upcoming steps to authorize the account and allow it to write to the bucket. 6. To proceed go to the Google Cloud Storage's[ Buckets](https://console.cloud.google.com/storage/browser) page and either select an existing bucket or create a new one to store the Event or Visuts Data reports from Adapty. Then navigate to the PERMISSIONS section and select the option to [GRANT ACCESS](https://support.google.com/cloudidentity/answer/9178892?hl=en). 7. In the PERMISSIONS section, input the Email of the service account obtained in the fifth step mentioned earlier, then choose the Storage Object Creator role. Finally, click on SAVE to apply the changes. Remember to keep the name of the bucket for future reference. 8. After passing these steps have successfully completed the necessary setup steps in the Google Cloud Console! The final step involves entering the bucket's name, accessing the JSON file containing the downloaded private key, and extracting the required field values for use in Adapty. ## Manual data export In addition to the automatic event data export to Google Cloud Storage, Adapty also provides a manual file export functionality. With this feature, you can select a specific time interval for the event data and export it to your GCS bucket manually. This allows you to have greater control over the data you export and when you export it. The specified date range will be used to export the events created from Date A 00:00:00 UTC up to Date B 23:59:59 UTC. ## Table structure In Google Cloud Storage integration, Adapty provides table to store historical data for transaction events and paywall visits. The table contains information about the user profile, revenue and proceeds, and the origin store, among other data points. Essentially, these tables log all transactions generated by an app for a given time period. :::warning Note that this structure may grow over time — with new data being introduced by us or by the 3rd parties we work with. Make sure that your code that processes it is robust enough and relies on the specific fields, but not on the structure as a whole. ::: Here is the table structure for the events: | Column | Description | |------|-----------| | **profile_id** | Adapty user ID. | | **event_type** | Lowercased event name. Refer to the [Events](events) section to learn event types. | | **event_datetime** | ISO 8601 date. | | **transaction_id** | A unique identifier for a transaction such as a purchase or renewal. | | **original_transaction_id** | The transaction identifier of the original purchase. | | **subscription_expires_at** | The Expiration date of subscription. Usually in the future. | | **environment** | Could be Sandbox or Production. | | **revenue_usd** | Revenue in USD. Can be empty. | | **proceeds_usd** | Proceeds in USD. Can be empty. | | **net_revenue_usd** | Net revenue (income after taxes) in USD. Can be empty. | | **tax_amount_usd** | Amount of money deducted for taxes in USD. Can be empty. | | **revenue_local** | Revenue in local currency. Can be empty. | | **proceeds_local** | Proceeds in local currency. Can be empty. | | **net_revenue_local** | Net revenue (income after taxes) in local currency. Can be empty. | | **tax_amount_local** | Amount of money deducted for taxes in local currency. Can be empty. | | **customer_user_id** | Developer user ID. For example, it can be your user UUID, email, or any other ID. Null if you didn't set it. | | **store** | Could be _app_store_ or _play_store_. | | **product_id** | Product ID in the Apple App Store, Google Play Store, or Stripe. | | **base_plan_id** | [Base plan ID](https://support.google.com/googleplay/android-developer/answer/12154973) in the Google Play Store or [price ID](https://docs.stripe.com/products-prices/how-products-and-prices-work#what-is-a-price) in Stripe. | | **developer_id** | Developer (SDK) ID of the paywall where the transaction originated. | | **ab_test_name** | Name of the A/B test where the transaction originated. | | **ab_test_revision** | Revision of the A/B test where the transaction originated. | | **paywall_name** | Name of the paywall where the transaction originated. | | **paywall_revision** | Revision of the paywall where the transaction originated. | | **profile_country** | Profile Country determined by Adapty, based on IP. | | **install_date** | ISO 8601 date when the installation happened. | | **idfv** | [identifierForVendor](https://developer.apple.com/documentation/uikit/uidevice/1620059-identifierforvendor) on iOS devices | | **idfa** | [advertisingIdentifier](https://developer.apple.com/documentation/adsupport/asidentifiermanager/advertisingidentifier) on iOS devices | | **advertising_id** | The Advertising ID is a unique code assigned by the Android Operating System that advertisers might use to uniquely identify a user's device | | **ip_address** | Device IP (can be IPv4 or IPv6, with IPv4 preferred when available). It is updated each time IP of the device changes | | **cancellation_reason** |

A reason why the user canceled a subscription.

Can be:

**iOS & Android**_voluntarily_cancelled_, _billing_error_, _refund_

**iOS** _price_increase_, _product_was_not_available_, _unknown_, _upgraded_

**Android** _new_subscription_replace_, _cancelled_by_developer_

| | **android_app_set_id** | An [AppSetId](https://developer.android.com/design-for-safety/privacy-sandbox/reference/adservices/appsetid/AppSetId) - unique, per-device, per developer-account user-resettable ID for non-monetizing advertising use cases. | | **android_id** | On Android 8.0 (API level 26) and higher versions of the platform, a 64-bit number (expressed as a hexadecimal string), unique to each combination of app-signing key, user, and device. For more details, see [Android developer documentation](https://developer.android.com/reference/android/provider/Settings.Secure#ANDROID_ID). | | **device** | The end-user-visible device model name. | | **currency** | The 3-letter currency code (ISO-4217) of the transaction. | | **store_country** | Profile Country determined by Apple/Google store. | | **attribution_source** | Attribution source. | | **attribution_network_user_id** | ID assigned to the user by attribution source. | | **attribution_status** | Can be organic, non_organic or unknown. | | **attribution_channel** | Marketing channel name. | | **attribution_campaign** | Marketing campaign name. | | **attribution_ad_group** | Attribution ad group. | | **attribution_ad_set** | Attribution ad set. | | **attribution_creative** | Attribution creative keyword. | | **attributes** | JSON of [custom user attributes](setting-user-attributes#custom-user-attributes). This will include any custom attributes you’ve set up to send from your mobile app. To send it, enable the **Send User Attributes** option in the [Integrations -> Webhooks](https://app.adapty.io/integrations/customwebhook) page. | Here is the table structure for the paywall visits: | Column | Description | | :-------------------- | :----------------------------------------------------------------------------------------------------------- | | **profile_id** | Adapty user ID. | | **customer_user_id** | Developer user ID. For example, it can be your user UUID, email, or any other ID. Null if you didn't set it. | | **profile_country** | Profile Country determined by Apple/Google store. | | **install_date** | ISO 8601 date when the installation happened. | | **store** | Could be _app_store_ or _play_store_. | | **paywall_showed_at** | The date when the paywall has been displayed to the customer. | | **developer_id** | Developer (SDK) ID of the paywall where the transaction originated. | | **ab_test_name** | Name of the A/B test where the transaction originated. | | **ab_test_revision** | Revision of the A/B test where the transaction originated. | | **paywall_name** | Name of the paywall where the transaction originated. | | **paywall_revision** | Revision of the paywall where the transaction originated. | ## Events and tags You can manage what data is communicated by the integration. The integration offers the following configuration options: | Setting | Description | | :--------------------------------- | :----------------------------------------------------------- | | **Exclude Historical Events** | Opt to exclude events that occurred before the user installed the app with Adapty SDK. This prevents duplication of events and ensures accurate reporting. For instance, if a user activated a monthly subscription on January 10th and updated the app with Adapty SDK on March 6th, Adapty will omit events before March 6th and retain subsequent events. | | **Include events without profile** | Opt to include transactions that are not linked to a user profile in Adapty. These may include purchases made before Adapty SDK was installed or transactions received from store server notifications that cannot be immediately associated with a specific user. | | **Send User Attributes** | If you wish to send user-specific attributes, like language preferences, and your OneSignal plan supports more than 10 tags, select this option. Enabling this allows the inclusion of additional information beyond the default 10 tags. Note that exceeding tag limits may result in errors. | Below the integration settings, there are three groups of events you can export, send, and store in Amazon S3 from Adapty. Simply turn on the ones you need. Check the full list of the events offered by Adapty [here](events). --- # File: google-platform-resources.md --- --- title: "Google Platform resources" description: "Explore Google platform resources to optimize subscription handling in your app." --- Adapty offers SDKs and integrations tailored for Google Platforms, simplifying the development of in-app purchases, subscriptions, paywalls, and A/B tests. Use the following resources to maximize the benefits Adapty provides for Google Platforms. ### Initial configuration in Google Play Console 1. [Enable Developer APIs in Google Play Console](enabling-of-devepoler-api) 2. [Create a service account in the Google Cloud Console](create-service-account) 3. [Generate service account key file](create-service-account-key-file) 4. [Grant permissions to a service account in the Google Play Console](grant-permissions-to-service-account) ### Products and offers configuration in Google Play Console 1. [Creating products for your mobile app](android-products) 2. [Creating offers to products](google-play-offers) ### Additional information 1. [Google Play Data Safety](google-play-data-safety) 2. [Apple app privacy](apple-app-privacy) 3. [Google Reduced Service Fee](google-reduced-service-fee) --- # File: google-play-data-safety.md --- --- title: "Google Play Data Safety" description: "Ensure compliance with Google Play Data Safety policies in Adapty." --- The Data Safety section available on Google Play provides a simple method for app developers to inform users about the data collected or shared by their app, as well as highlight their app's critical privacy and security measures. This information enables users to make more informed decisions when selecting which apps to download and use. Here is a short guide on data that Adapty collects to help you provide the required information to Google Play. ## Data Collection and Security **Does your app collect or share any of the required user data types?** Select 'Yes' as Adapty collects a customer's purchase history. **Is all of the user data collected by your app encrypted in transit?** Select 'Yes' as Adapty encrypts data in transit. **Do you provide a way for users to request that their data is deleted?** If selecting 'Yes', ensure your customers have a way to contact your support team to request a data deletion. You will be able to delete the customer directly from the Adapty dashboard or via REST API. ## Data Types Here is a list of the data types that Google requires for reporting, and we have specified whether Adapty collects any particular type of data. | Data Type | Details | | :---------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Location | Is not collected by Adapty | | Health and Fitness | Is not collected by Adapty | | Photos and Videos | Is not collected by Adapty | | Files and Docs | Is not collected by Adapty | | Calendar | Is not collected by Adapty | | Contacts | Is not collected by Adapty | | User Content | Is not collected by Adapty | | Browsing History | Is not collected by Adapty | | Search History | Is not collected by Adapty | | App Info and Performance | Is not collected by Adapty | | Web Browsing | Is not collected by Adapty | | Contact Info | Is not collected by Adapty | | Financial Info | Adapty collects purchase history from users | | Personal Info and Identifiers | Adapty collects User ID and some other identifiable contact information including name, email address, phone number, etc, if you explicitly pass them to Adapty SDK. | | Device and other identifiers | Adapty collects data on device id. | ## Data usage and handling ### User IDs **1. Is this data collected, shared, or both?** This data is collected by Adapty. If you are using integrations set up between Adapty and third parties that are not considered service providers, you may need to disclose "Shared" here as well. **2. Is this data processed ephemerally?** Select 'No'. **3. Is this data required for your app, or can users choose whether it's collected?** This data collection is required and cannot be turned off. **4. Why is this user data collected? / Why is this user data shared?** Select the 'App functionality' and 'Analytics' checkboxes. ### Financial Info If you are using Adapty, you must disclose that your app collects 'Purchase history' information from the Data types section in Google Play Console. ### Device or other IDs ## Next Steps Once you have made your data safety selections, Google will display a preview of your app's privacy section. If you have opted for "Financial Info" and "Device or other IDs" as mentioned earlier, your privacy information should appear similar to the following example If you are prepared to submit your app for App Review, please refer to our [Release Checklist](release-checklist) document for further guidance on preparing your app for submission. --- # File: google-play-offers.md --- --- title: "Offers in Google Play" description: "Configure Google Play offers to improve app monetization and retention." --- With Billing Library v5, Google introduced a new way of working with offers. It gives you much more flexibility, but it's important to configure them properly. After reading this short guide from Adapty, you'll have a full understanding of Google Play Offers. :::note Checklist to successfully use Google Play offers 1. [Create and activate](google-play-offers#configuring-offers-in-google-play) offers in Google Play Console. 2. [Add](google-play-offers#adding-offers-to-adapty-products) offers to Adapty Products. 3. [Choose](google-play-offers#choosing-the-offer-in-adapty-paywalls) the offer to use in Adapty Paywall. 4. Use Adapty SDK 2.6 or newer. 5. [Check eligibility criteria](google-play-offers#configuring-offers-in-google-play) for the offer in Google Play Console if everything is configured, but the offer is not applied. ::: ## Overview Before Google Play Billing Library v5 a subscription could only have one offer. If you wanted to test different offers, for example, a 3-day free trial vs a 1-week free trial, you would have to create 2 different subscriptions, which is not optimal. Now you can create multiple offers for every base plan (previously known as subscription) and this means that you have to decide which offer should be used at a given moment. Please check the docs on [base plans](android-products) if you're not familiar with them. ## Configuring offers in Google Play In the screenshot above, you can see a subscription `premium_access`(1) with two base plans: `1-month` (2) and `1-year` (3). Offers are always created for base plans. 1. To create an offer, click **Add offer** and choose the base plan from the list. 2. Enter the offer ID. It will be later used in the analytics and Adapty dashboard, so give it a meaningful name. 3. Choose the eligibility criteria: 1. **New customer acquisition**: the offer will be available only to new subscribers if they haven't used this offer in the past. This is the most common option and should be used by default. 2. **Upgrade**: this offer will be available for the customers upgrading from the other subscription. Use it when you want to promote more expensive plans to your existing subscribers, for example, customers upgrading from the bronze to the gold tier of your subscription. 3. **Developer determined**: you can control who can use this offer from the app code. Be cautious using it in production to avoid possible fraud: customers can activate a free or discounted subscription over and over again. A good use case for this offer type is winning back churned subscribers. 4. Add up to two pricing phases to your offer. There are three phase types available: 1. **Free trial**: the subscription can be used for free for a configured amount of time (minimum 3 days). This is the most common offer. 2. **Single payment**: the subscription is cheaper if the customers pay upfront. For example, normally a monthly plan costs $9.99, but with this offer type, the first three months cost $19.99, a 30% discount. 3. **Discounted recurring payment**: the subscription is cheaper for the first `n` periods. For example, normally a monthly plan costs $9.99, but with this offer type, each of the first three months costs $4.99, a 50% discount. An offer can have two phases. In this case, the first phase must be a Free trial, and the second one is either a Single payment or a Discounted recurring payment. They would be applied in this order.

:::info Please note that paywalls created with the Adapty Paywall Builder will display only the first phase of a multi-phase Google subscription offer. However, rest assured that when a user purchases the product, all offer phases will be applied as configured in Google Play. ::: 5. Activate the offer to use it in the app. After activating the offer, you should copy its ID to use in Adapty. The `1-month` plan has three offers: `free-trial-1-week` (1), `free-trial-3-days` (2), `pay-up-front-3months-30p` (3). The `1-year` plan has one offer: `free-trial-1-week` (1). As you can see, offer IDs can be the same for different base plans. ## Adding offers to Adapty products Let's create a 1-month product in Adapty with all the offers. You can do it from a single screen. 1. Choose the name, access level, and period. 2. Copy the Product ID and Base plan ID from Google Play Console and paste them into the corresponding fields in Adapty. 3. Copy an offer ID from Google Play Console and paste it into the Google Play Offer ID field in Adapty. Provide a user-friendly name for the offer. If you have multiple offers, add all of them by clicking **Add offer**. 4. Save the changes. ## Choosing the offer in Adapty paywalls Finally, you have to choose, which offer should be displayed on the given paywall. When creating a paywall or editing a draft of the paywall, choose the offer from the dropdown next to the product. This offer will be then used during the purchase from the paywall if the customer is eligible for the offer in the first place. If you configure a paywall like this, a monthly subscription will not have a free trial. A yearly subscription will have a 1-week trial if the customer is eligible. :::note If you can't edit the products on the paywall, it means that the paywall is not in the draft state. You can duplicate it or create a new paywall, and then [select the new paywall in the placement](add-audience-paywall-ab-test) . ::: --- # File: google-play-store-connection-configuration.md --- --- title: "Configure Google Play Store integration" description: "Configure Google Play Store connection in Adapty for smooth in-app purchase handling." --- This section outlines the integration process for your mobile app sold via Google Play with Adapty. You'll need to input your app's configuration data from the Play Store into the Adapty Dashboard. This step is crucial for validating purchases and receiving subscription updates from the Play Store within Adapty. You can complete this process during the initial onboarding or make changes later in the **App Settings** of the Adapty Dashboard. :::danger Configuration change is only acceptable until you release your mobile app with integrated Adapty paywalls. The change after the release will break the integration and the paywalls will stop showing in your mobile app. ::: ## Step 1. Provide Package name The Package name is the unique identifier of your app in the Google Play Store. This is required for the basic functionality of Adapty, such as subscription processing. 1. Open the [Google Play Developer Console](https://play.google.com/console/u/0/developers). 2. Select the app whose ID you need. The **Dashboard** window opens. 3. Find the product ID under the application name and copy it. 4. Open the [**App settings**](https://app.adapty.io/settings/android-sdk) from the Adapty top menu. 5. In the **Android SDK** tab of the **App settings** window, paste the copied **Package name**. ## Step 2. Upload the account key file 1. Upload the service account private key file in JSON format that you have created at the [Create service account key file](create-service-account) step into the **Service account key file** area. Don't forget to click the **Save** button to confirm the changes. **What's next** - [Enable Real-time developer notifications (RTDN) in the Google Play Console](enable-real-time-developer-notifications-rtdn) --- # File: google-reduced-service-fee.md --- --- title: "Google Reduced Service Fee" description: "Understand Google’s reduced service fee and how it impacts app revenue." --- Learn how Adapty can help you manage your proceeds on Google Play Store, including the reduced service fee program for developers earning less than $1,000,000 USD annually. By following the necessary steps to join the program and updating your membership status in the Adapty Sashboard, you can ensure that your sales commission is accurately calculated, and you receive reliable information on your transactions. Adapty also supports the Small Business Program for App Store. You can reference [this document](app-store-small-business-program) for more details. ## Google's 15% Reduced Service Fee Developers who earn less than $1,000,000 USD annually are eligible to join a reduced service fee tier offered by Google. Under this tier, the commission fee is lowered to 15% instead of the standard rate of 30%. Developers offering automatically renewing subscription products are eligible for a reduced service fee of 15%, independent of their participation in other programs offered by Google Play. You can read more about the service fees [here](https://support.google.com/googleplay/android-developer/answer/112622?hl=en). To participate in the Google Play Reduced Service Fee program, you must have a payment profile and create an Account Group where your Developer Account is the Primary Developer Account. You also need to inform Google if you have any Associated Developer Accounts (ADAs) and link your account to the group. Once these steps are completed, you will automatically be enrolled in the program, and you'll be eligible for the reduced commission rate on your first $1,000,000 USD in revenue per year. For more detailed information on the necessary steps, please refer to Google's [documentation](https://support.google.com/googleplay/android-developer/answer/10632485). ## How Adapty calculates the proceeds Play Store Adapty can accurately calculate your app's earnings by deducting Google's commissions and taking into account your eligibility for the Reduced Service Fee program. In the Adapty Dashboard, the Reduced Service Fee membership status is assigned to each individual app based on the developer's representation of multiple apps from different companies in their account. This means that the eligibility for the program is determined on a per-app basis. You can add multiple periods by selecting a range of dates for each period in the same field. The date range represents the start and end date of the period during which your business was a member of the Small Business Program. Please note that the Entry Date refers to the earliest date in the range when your business became a member of the program, and the Exit Date refers to the latest date in the range when your business officially left or was removed from the program. You can select your entry date according to your preference. However, it's important to note that if you select a past date, any webhooks and integration events already processed will not be resent with corrected pricing data. To ensure the accuracy of pricing data sent to your integrations, it's advisable to set your effective entry date as soon as possible. This way, you can receive reliable and up-to-date information on your transactions and make informed decisions accordingly. ## Letting Adapty know To manage your Reduced Service Fee membership status for Google Play, go to the **App Settings > General tab** in your Adapty account. Click the **Add period** button to specify your membership status for a specific period range. In the "Period" field, select a date range that indicates your business's membership start and end dates. This range can include any date in the past or the future. You can add additional membership periods by clicking on the "Add Period" button again. You can select your period start according to your preference. However, if you select a past period start, any webhooks and integration events already processed will not be resent with corrected pricing data. To ensure the accuracy of pricing data sent to your integrations, it's advisable to set your effective period start as soon as possible. This way, you can receive reliable and up-to-date information on your transactions and make informed decisions accordingly. Please note that the Reduced Service Fee membership status will only apply to the specific period range you've specified. Once the period end is reached, you'll need to add another period if you want to continue with the Reduced Service Fee membership status. To ensure that we calculate your sales commission correctly, please enter the exit date in the Adapty Dashboard app settings as soon as possible if your business has left the Reduced Service Fee. If no exit date is provided, we will continue to calculate your commission based on the reduced rate. --- # File: grace-period.md --- --- title: "Grace period" description: "Understand how subscription grace periods work and improve user retention." --- The Grace period chart displays the number of subscriptions that have entered the grace period state due to a billing issue. During this period, the subscription remains active while the store tries to receive payment from the subscriber. If payment is not successfully received before the grace period ends, the subscription enters the billing issue state. ### Calculation Adapty calculates the Grace period chart by tracking the number of subscriptions that have entered the Grace period state within a given time period. The Grace period begins when the subscription enters the billing issue state due to a payment failure and ends after a specified amount of time (6 days for weekly subscriptions, 16 days for all other subscriptions) or when payment is successfully received. The chart provides insights into the effectiveness of the grace period feature and can help identify potential issues with payment processing or subscription management. ### Available filters and grouping - ✅ Filter by: Attribution, country, paywall, store, product, and duration. - ✅ Group by: Product, country, store, paywall, duration, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative. You can find more information about the available controls, filters, grouping options, and how to use them in the[ this documentation.](controls-filters-grouping-compare-proceeds) ### Similar metrics In addition to the Grace period chart, Adapty also provides metrics for other issues-related events, such as Refund events, Refund money, and Billing issue. To learn more about these issue-related metrics, please refer to the following documentation: - [Refund money](new-trials) - [Refund events](active-trials) - [Billing issue](billing-issue) --- # File: grant-permissions-to-service-account.md --- --- title: "Grant permissions to service account in the Google Play Console" description: "Grant permissions to service accounts for secure and efficient API access." --- Grant the required permissions to the service account that Adapty will use to manage subscriptions and validate purchases. 1. Open the [**Users and permissions**](https://play.google.com/console/u/0/developers/8970033217728091060/users-and-permissions) page in the Google Play Console and click the **Invite new users** button. 2. In the **Invite user** page, enter the email of the service users you've created. 3. Switch to the **Account permissions** tab. 4. Select the following permissions: - View app information and download bulk reports (read-only) - View financial data, orders, and cancellation survey responses - Manage orders and subscriptions - Manage store presence 5. Click the **Invite user** button. 6. In the **Send invite?** window, click the **Send invite** button. The service account will show in the user list. **What's next** - [Generate the service account key file in the Google Play Console](create-service-account-key-file) --- # File: handle-paywall-actions.md --- --- title: "Respond to button actions in iOS SDK" description: "Handle paywall button actions in iOS using Adapty for better app monetization." toc_max_heading_level: 4 --- 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 `close` action that dismisses the paywall. :::info In the iOS, 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. ::: ```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: how-adapty-analytics-works.md --- --- title: "How Adapty analytics works" description: "Learn how Adapty analytics work to track subscription performance efficiently." --- Adapty Analytics offers a powerful suite of tools that provide valuable insights into your user base, allowing you to make data-driven decisions and optimize your app's performance. With Adapty, you can go beyond basic metrics and dive deep into advanced analytics such as funnels, cohorts, retention, lifetime value (LTV) charts, and more. Let's explore the general approach to data gathering, calculation, and the various analytics features available. To learn more about specific metrics and advanced analytics features, please refer to the relevant sections in the documentation menu. To get started with Adapty analytics, simply [iOS](sdk-installation-ios), [Android](sdk-installation-android), [Flutter](sdk-installation-flutter), [React Native](sdk-installation-reactnative), and [Unity](sdk-installation-unity) the Adapty SDK for your iOS or Android apps. From there, you can analyze close to real-time metrics with [advanced controls](controls-filters-grouping-compare-proceeds), such as filters and grouping, including country, paywall, product, and more. By utilizing this powerful tool, you can gain a deeper understanding of your user base and make data-driven decisions to optimize your app's performance. ### Close to real-time data for advanced analysis When it comes to analytics, the Apple App Store and Google Play Store offer some basic metrics such as downloads, revenue, and retention rates. More than that, the data is usually updated only once a day, which can limit your ability to make real-time decisions. In contrast, Adapty offers close to real-time analytics that allows you to track key metrics as they happen, giving you an accurate and up-to-date view of your app's performance. Data coming to Adapty gets processed and ends up in analytics 15 to 30 minutes after the event has been received. This extra-processing step is necessary to make sure our analytics is fast and reliable. With Adapty, you can access advanced metrics and filters that provide deeper insights into user behavior, such as ad network, ad campaign, country, paywall, product, and more. The analytics data is generated using the current state of purchase receipts stored in Adapty, so they always reflect up-to-date information without relying on any client-side event logging. This means you can access real-time metrics without any delay, ensuring that you always have the latest information on hand. Additionally, Adapty allows you to attribute analytics data to specific customers, including data from [Apple Search Ads](apple-search-ads) and mobile measurement partners (Appsflyer, Adjust, Branch, etc). This gives you a complete understanding of your users and enables you to personalize your app's marketing and engagement strategies. ### Data attribution One of the advantages of Adapty analytics is that all data is attributed to every device, which allows for more precise segmentation based on device type, location, and other factors. This provides deeper insights into user behavior and enables you to tailor your marketing and engagement strategies to specific audiences. Additionally, Adapty offers robust CRM functionality that allows you to track and analyze user behavior across multiple touchpoints, including in-app purchases, and subscriptions. This provides a complete view of the customer journey and helps you identify opportunities to optimize user acquisition, retention, and monetization. Learn more about the [CRM](profiles-crm?_ga=2.76111573.1535148052.1683526474-1601936383.1676319293) in the Adapty system. ### Data from both stores In addition to providing a unified view of data from both the App Store and Play Store, Adapty analytics also enables you to group and filter data by store. This allows you to compare and contrast metrics between the two stores and gain deeper insights into user behavior and preferences. For example, you can analyze how different user segments behave in each store, such as comparing retention rates for iOS with Android users or analyzing revenue generated by users in different countries in each store. You can also filter by different app versions, pricing tiers, and more, to gain granular insights into user behavior and make informed decisions. ### Adapty events and integrations In addition to providing store-specific metrics, Adapty also offers a powerful set of subscription events that can be used to gain deeper insights into user behavior. With 17 subscription events that can be sent to 3rd party analytics and webhook, you can gain granular insights into user behavior and optimize your subscription strategies for maximum impact. Check our [documentation](events) to learn more about Adapty events and 3rd party integrations. ### Historical data import Another advantage of Adapty's analytics platform is the ability to [import historical data](importing-historical-data-to-adapty), giving you a more comprehensive view of your app's performance over time. This feature allows you to see transaction data from before you started using Adapty and organize it into cohorts for more granular analysis. Adapty's analytics platform also offers advanced analytics and integrations with a range of third-party tools, allowing you to gain deeper insights into user behavior and optimize your subscription offerings accordingly. With Adapty, you can easily track key metrics such as conversion rates, churn, and lifetime value, as well as gain insights into user segments and cohorts. ### Commission fee and taxes calculation Adapty Analytics provides accurate calculation of commission fees and taxes associated with transactions. **Commission fee calculation:** Adapty supports commission calculation for both the App Store and Play Store. The calculation is based on the Gross revenue, representing the total revenue generated by transactions before any deductions. For detailed information on how Adapty calculates store commission fees, please refer to the corresponding documentation for [App Store](app-store-small-business-program) and [Play Store.](google-reduced-service-fee) **Tax calculation**: Adapty supports tax calculation for both the App Store and Play Store transactions. We incorporate tax calculation into its revenue analytics. The platform considers the user's store account country VAT tax rate for the transaction to accurately calculate the taxes associated with each transaction. The taxes are deducted from the revenue after the commission fee is applied. ### Predictions Adapty's analytics platform offers predictive capabilities specifically for cohort analyses, focusing on Lifetime Value (LTV) and revenue predictions. By leveraging historical data and advanced machine learning algorithms, Adapty can generate predictions for these key metrics, providing valuable insights for planning and optimizing your app's growth strategies. To learn more about utilizing predictions for cohort analyses and understanding how to interpret and apply the forecasted insights, refer to the detailed documentation on [Predictions in cohorts](predicted-ltv-and-revenue). By leveraging Adapty's advanced analytics capabilities, you can make data-driven decisions and optimize your app's performance for success. --- # File: identifying-users.md --- --- 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. ## Set appAccountToken on iOS 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 } } ``` --- # File: implement-observer-mode-android.md --- --- 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: implement-observer-mode-flutter.md --- --- 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: implement-observer-mode-react-native.md --- --- title: "Implement Observer mode in React Native SDK" description: "Implement observer mode in Adapty to track user subscription events in React Native 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 [React Native](sdk-installation-reactnative#configure-adapty-sdk). 2. [Report transactions](report-transactions-observer-mode-react-native) 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 title="App.tsx" adapty.activate('YOUR_PUBLIC_SDK_KEY', { observerMode: true, // Enable observer mode }); ``` 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-react-native.md). 3. [Associate paywalls](report-transactions-observer-mode-react-native) with purchase transactions. --- # File: implement-observer-mode-unity.md --- --- title: "Implement Observer mode in Unity SDK" description: "Implement observer mode in Adapty to track user subscription events in Unity 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 [Unity](sdk-installation-unity#configure-adapty-sdk). 2. [Report transactions](report-transactions-observer-mode-unity) 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. ::: ```csharp showLineNumbers title="C#" using UnityEngine; using AdaptySDK; public class AdaptyListener : MonoBehaviour, AdaptyEventListener { void Start() { DontDestroyOnLoad(this.gameObject); Adapty.SetEventListener(this); var builder = new AdaptyConfiguration.Builder("YOUR_PUBLIC_SDK_KEY") .SetObserverMode(true); // Enable observer mode Adapty.Activate(builder.Build(), (error) => { if (error != null) { // handle the error return; } }); } } ``` 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-unity.md). 3. [Associate paywalls](report-transactions-observer-mode-unity) with purchase transactions. --- # File: implement-observer-mode.md --- --- 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: importing-historical-data-to-adapty.md --- --- title: "Importing historical data to Adapty" description: "Import historical data into Adapty for detailed analytics." --- After installing the Adapty SDK and releasing your app, you can access your users and subscribers in the [Profiles](profiles-crm) section. But what if you have legacy infrastructure and need to migrate to Adapty, or simply want to see your existing data in Adapty? :::note Data import is not mandatory Adapty will automatically grant access levels to historical users and restore their purchase events once they open the app with the Adapty SDK integrated. For this use case, importing historical data is not necessary. However, importing data ensures precise analytics if you have a significant number of historical transactions, although it is generally not required for migration. ::: To import data to Adapty: 1. Export your transactions to a CSV file (separate files should be provided for iOS, Android, and Stripe). Please refer to the [Import file format section](importing-historical-data-to-adapty#import-file-format) below for detailed requirements. 2. If any file exceeds 1 GB, prepare a data sample with approximately 100 lines. 3. Upload all the files to Google Drive (you can compress them, but keep them separate). 4. For iOS transactions, ensure the **In-app purchase API** section in the [**App settings**](https://app.adapty.io/settings/ios-sdk)is filled out with the **Issuer ID**, **Key ID**, and the **Private key** (.P8 file) even if you use the StoreKit 1. See the [Provide Issuer ID and Key ID](app-store-connection-configuration#step-2-provide-issuer-id-and-key-id) and [Upload In-App Purchase Key file](app-store-connection-configuration#step-3-upload-in-app-purchase-key-file) sections for detailed instructions. 5. Share the links with our team via [email](mailto:support@adapty.io) or through the online chat in the Adapty Dashboard. Do not worry, importing historical data will not create duplicates, even if that data overlaps with existing entries in Adapty. ## Known limitations for Android 1. Only active subscriptions will be restored; expired transactions will not be. 2. Only the latest renewals in a subscription will be restored; the entire chain of purchases will not be. 3. If the product price has changed since the purchase, the current price will be used, which may result in incorrect pricing. ## Import file format Please prepare your data in a file or files that meet the following rules: - [ ] The file format is .CSV. - [ ] Separate files for Android, iOS, and Stripe imports. - [ ] Every import file contains all [required columns](importing-historical-data-to-adapty#required-fields). - [ ] The columns in the import file(s) have headers. - [ ] The column headers are exactly as in the **Column name** column in the table below. Please check for typos. - [ ] Columns that are not required can be absent from the file. Don't add empty columns for data you don't have. - [ ] Import files should not have extra columns not mentioned in the table. If present, please delete them. - [ ] Values are separated by commas. - [ ] Values are not enclosed in quotes. - [ ] If there are several **apple_original_transaction_id**'s for one user, add all of them as separate lines for each **apple_original_transaction_id**. Otherwise, we may not be able to restore consumable purchases. Please use the following files as samples for [iOS](https://docs.google.com/spreadsheets/d/1GTw7cqL0wkDrbXGUTSS3TrdeypvJnrD6h7Jd-SYnmiY/edit?usp=sharing) and [Android](https://docs.google.com/spreadsheets/d/1i68ZMorGDnfUoveoMOI53FKYPc9UWNChapbeVTQSGks/edit?usp=sharing). ### Available import file columns | Column name | Presence | Description | |-----------|--------|-----------| | **user_id** | required | ID of your user | | **apple_original_transaction_id** | required for iOS |

The original transaction ID or OTID ([learn more](https://developer.apple.com/documentation/appstoreserverapi/originaltransactionid) ), used in StoreKit 2 import mechanism. As one user can have multiple OTIDs, it is enough to provide at least one for successful import.

**Note:** We require In-app purchase API credentials for this import to be set up in your Adapty Dashboard. Learn how to do it [here](app-store-connection-configuration#step-3-upload-in-app-purchase-key-file) .

| | **google_product_id** | required for Google | Product ID in the Google Play Store. | | **google_purchase_token** | required for Google | A unique identifier that represents the user and the product ID for the in-app product they purchased | | **google_is_subscription** | required for Google | Possible values are `1` \| `0` | | **stripe_token** | required for Stripe | Token of a Stripe object that represents a unique purchase. Could either be a token of Stripe's Subscription (`sub_...`) or Payment Intent (`pi_...`). | | **subscription_expiration_date** | optional | The date of subscription expiration, i.g. next charging date, date, and time with timezone (2020-12-31T23:59:59-06:00) | | **created_at** | optional | Date and time of profile creation (2019-12-31 23:59:59-06:00) | | **birthday** | optional | The birthday of the user in format 2000-12-31 | | **email** | optional | The e-mail of your user | | **gender** | optional | The gender of the user | | **phone_number** | optional | The phone number of your user | | **country** | optional | format [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) | | **first_name** | optional | The first name of your user | | **last_name** | optional | The last name of your user | | **last_seen** | optional | The date and time with timezone (2020-12-31T23:59:59-06:00) | | **idfa** | optional | The identifier for advertisers (IDFA) is a random device identifier assigned by Apple to a user's device. Applicable to iOS apps only | | **idfv** | optional | The identifier for vendors (IDFV) is a unique code assigned to all apps developed by a single developer, which in this case refers to your apps. Applicable to iOS apps only | | **advertising_id** | optional | The Advertising ID is a unique code assigned by the Android Operating System that advertisers might use to uniquely identify a user's device | | **amplitude_user_id** | optional | The user ID from Amplitude | | **amplitude_device_id** | optional | The device ID from Amplitude | | **mixpanel_user_id** | optional | User ID from Mixpanel | | **appmetrica_profile_id** | optional | User profile ID from AppMetrica | | **appmetrica_device_id** | optional | The device ID from AppMetrica | | **appsflyer_id** | optional | Unique identifier from AppsFlyer | | **adjust_device_id** | optional | The device ID from Adjust | | **facebook_anonymous_id** | optional | A unique identifier generated by Facebook for users who interact with your app or website anonymously, meaning they are not logged into Facebook | | **branch_id** | optional | Unique identifier from Branch | | **attribution_source** | optional | The source integration of the attribution, for example, appsflyer | | **attribution_status** | optional | organic | | **attribution_channel** | optional | The attribution channel that brought the transaction | | **attribution_campaign** | optional | The attribution campaign that brought the transaction | | **attribution_ad_group** | optional | The attribution ad group that brought the transaction | | **attribution_ad_set** | optional | The attribution ad set that brought the transaction | | **attribution_creative** | optional | Specific visual or textual elements used in an advertisement or marketing campaign that are tracked to determine their effectiveness in driving desired actions, such as clicks, conversions, or installs | | **custom_attributes** | optional | Define up to 30 custom attributes as a JSON dictionary in key-value format:
  • **key**: (string) The name of the custom attribute
  • **value**: (string, integer, float, or boolean) The value of the custom attribute.

Format: `"{'string_value': 'some_value', 'float_value': 123.0, 'int_value': 456}"`.

Note the use of double and single quotes in the format. Keep in mind that booleans and integers will be converted to floats.

| ### Required Fields There are 2 groups of required fields for each platform: **user_id** and data identifying purchases specific to the corresponding platform. Refer to the table below for the mandatory fields per platform. | Platform | Required fields | |--------|---------------| | iOS |

user_id

apple_original_transaction_id

| | Android |

user_id

google_product_id

google_purchase_token

google_is_subscription

| | Stripe |

user_id

stripe_token

| Without these fields, Adapty won't be able to fetch transactions. For precise cohort analytics, please specify `created_at`. If not provided, we will assume the install date to be the same as the first purchase date. ### Import data to Adapty Please contact us and share your import files via [support@adapty.io](mailto:support@adapty.io) or through the online chat in the [Adapty Dashboard](https://app.adapty.io/overview). --- # File: initial-android.md --- --- title: "Initial integration with Google Play" description: "Get started with Adapty on Android and set up your app for efficient subscription management." --- We're thrilled to have you on board with Adapty! Our priority is to help you hit the ground running and achieve the best possible outcomes. This guide is designed to get you started with Adapty if your app is available in the Google Play Store. Integrating Adapty into your mobile app involves establishing connections between your app and Adapty at both Google Play and SDK levels. While the process may appear extensive, following the built-in onboarding in the Adapty Dashboard or the instructions below will simplify it, typically taking no more than 1 hour. ## Checklist for the initial integration - [ ] Once you create an account in Adapty and provide your mobile app name and category, we set up the app for you within our Adapty platform. - [ ] [Enable Developer APIs](enabling-of-devepoler-api) in Google Cloud Console - [ ] [Create a service account](create-service-account) in the Google Cloud Console - [ ] [Grant permissions to the service account](grant-permissions-to-service-account) in the Google Play Console - [ ] [Generate the service account key file](create-service-account-key-file) in the Google Cloud Console - [ ] [Configure Google Play integration](google-play-store-connection-configuration) itself in the Adapty Dashboard - [ ] [Enable Real-time developer notifications (RTDN)](enable-real-time-developer-notifications-rtdn) in the Google Play Console - [ ] Install and configure AdaptySDKs (you may install SDKs for one or more frameworks, whatever are needed) - [ ] [Install Adapty SDKs for Android](sdk-installation-android) - [ ] [Install Adapty SDKs for Flutter](sdk-installation-flutter) - [ ] [Install Adapty SDKs for React Native](sdk-installation-reactnative) - [ ] [Install Adapty SDKs for Unity](sdk-installation-unity) - [ ] Build your application and run it. Running as a snapshot or in a sandbox environment is sufficient. :::note It takes at least 24 hours for changes to take effect but there's a [hack](https://stackoverflow.com/a/60691844). In [Google Play Console](https://play.google.com/apps/publish/), open any application and in the **Monetize** section go to **Products** -> **Subscriptions**/**In-app products**. Change the description of any product and save the changes. Everything should be working now, you can revert in-app changes. ::: After the initial integration is complete, you [can begin using Adapty's features](product). Please keep in mind that for paywalls and products to be displayed in your mobile application, and for analytics to work, you need to make changes to your app's code. Specifically, you need to [display the paywalls](android-quickstart-paywalls) at least and, if you're using paywalls not created with the Paywall Builder, [handle the purchase process](android-making-purchases) within your app. :::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. ::: --- # File: initial_ios.md --- --- title: "Initial integration with the App Store" description: "Get started with Adapty on iOS to streamline subscription setup and management." --- We're thrilled to have you on board with Adapty! Our priority is to help you hit the ground running and achieve the best possible outcomes for your app. This guide is designed to get you started with Adapty if your app is available in the App Store. Integrating Adapty into your mobile app involves establishing connections between your app and Adapty at both the App Store and SDK levels. Though it may seem hard on the surface, following the onboarding in Adapty Dashboard or these instructions will help you accomplish this in no more than 30 minutes. ## Guide for the initial integration - [ ] Once you create an account in Adapty and provide your mobile app name and category, we set up the app for you within our Adapty platform. - [ ] [Generate In-App Purchase Key](generate-in-app-purchase-key) in the App Store Connect - [ ] [Configure App Store integration](app-store-connection-configuration) itself in the Adapty dashboard and App Store Connect - [ ] If your app has trials or other promotional offers, [configure App Store promotional offers](app-store-connection-configuration#step-4-for-trials-and-special-offers--set-up-promotional-offers) in the Adapty dashboard. - [ ] [Enable App Store server notifications](enable-app-store-server-notifications) in the App Store Connect - [ ] Install AdaptySDKs for the frameworks you're using: - [ ] [Install Adapty SDKs for native iOS](sdk-installation-ios) - [ ] Build your application and run it in sandbox mode. After the initial integration is complete, you [can begin using Adapty's features](product). Please keep in mind that for paywalls and products to be displayed in your mobile application, and for analytics to work, you need to make changes to your app's code. Specifically, you need to [display the paywalls](ios-quickstart-paywalls.md) at least and, if you're using paywalls not created with the Paywall Builder, [handle the purchase process](making-purchases) within your app. :::danger Go through release checklist before releasing your app Before releasing your application, make sure to carefully review the [Release Checklist](release-checklist) . This will ensure that you've completed all the necessary steps before your app goes live with Adapty SDK onboard. ::: --- # File: installation-of-adapty-sdks.md --- --- title: "Installation of Adapty SDK" description: "Install Adapty SDKs for iOS, Android, and cross-platform apps." --- You have three paths to get started depending on your preferences: - **Follow platform-specific quickstart guides**: Guides contain production-ready code snippets, so implementation doesn't take long. - [iOS](ios-sdk-overview.md) - [Android](android-sdk-overview.md) - [Flutter](flutter-sdk-overview.md) - [React Native](react-native-sdk-overview.md) - [Unity](unity-sdk-overview.md) - **Use LLMs**: Our docs are LLM-friendly. Read our [guide](adapty-cursor.md) on how to get the most out of using LLMs with the Adapty documentation. - **Explore sample apps**: - [iOS (Swift)](https://github.com/adaptyteam/AdaptySDK-iOS/tree/master/Examples) - [Android (Kotlin)](https://github.com/adaptyteam/AdaptySDK-Android) - [Flutter (Dart)](https://github.com/adaptyteam/AdaptySDK-Flutter/tree/master/example) - [React Native (Pure RN)](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples/AdaptyRnSdkExample) - [React Native (Expo)](https://github.com/adaptyteam/Focus-Journal-React-Native-Expo) - [Unity (C#)](https://github.com/adaptyteam/AdaptySDK-Unity) --- # File: installs.md --- --- title: "Installs" description: "Track app installs and understand their impact on subscriptions with Adapty." --- The Installs chart shows the total number of users who have installed the app for the first time, as well as any reinstalls by existing users. This includes multiple installations by the same user on different devices. Please note that incomplete downloads or installations that are canceled before completion are not counted toward the install count. ### Calculation Adapty’s Installs chart counts the total number of times the app has been installed, including by both new and existing users, as well as any reinstalls on different devices. However, incomplete installs or downloads canceled before finishing are not counted. You can define what qualifies as a new install event—whether it’s an installation on a specific device or one made by a specific user. Since a single user can have more than one device, this choice may affect your results. Set this in [**App Settings**](https://app.adapty.io/settings/general) under the [Installs definition for analytics](general#4-installs-definition-for-analytics) parameter. If you’re using the legacy **Installs definition for analytics** option based on profiles, the Installs chart might also include counts of new logged-in users who have accessed your app multiple times. ### Available filters and grouping - ✅ Filter by: Attribution, country, and store. - ✅ Group by: Country, store, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative. You can find more information about the available controls, filters, grouping options, and how to use them in [this documentation.](controls-filters-grouping-compare-proceeds) ### Installs chart usage The Installs chart provides a useful metric to track the overall growth of the user base. By analyzing the chart, you can gain insights into the number of new users who have installed their app for the first time, as well as any reinstalls by existing users. This information can help to identify trends and patterns in user acquisition over time, and make informed decisions about marketing and promotional activities. --- # File: integrate-payments.md --- --- title: "Integrate with stores or payment platforms" description: "Integrate Adapty with App Store, Google Play, custom stores, Stripe, and Paddle." --- To get started with Adapty, first integrate with the stores where your users buy products. Adapty connects to various app stores and web payment providers, bringing all your in-app purchases and analytics together in one place. ## Integrate with stores and web payments Choose your store below for detailed integration steps: - App Store - Google Play - Web payments: - Stripe - Paddle - Other stores ## Next steps Once you’ve connected your store or payment platform, you can move on to [adding products](quickstart-products.md). --- # File: ios-check-subscription-status.md --- --- title: "Check subscription status in iOS SDK" description: "Learn how to check subscription status in your iOS app with Adapty." displayed_sidebar: sdkios --- 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 { private var currentProfile: AdaptyProfile? nonisolated func didLoadLatestProfile(_ profile: AdaptyProfile) { self.currentProfile = profile // Update UI, unlock content, etc. } func hasAccess() -> Bool { return currentProfile?.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false } } // 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-deal-with-att.md --- --- 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 if #available(iOS 14, macOS 11.0, *) { 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: ios-display-legacy-pb-paywalls.md --- --- title: "Display legacy Paywall Builder paywalls in iOS SDK" description: "Learn how to display legacy Paywall Builder paywalls in your iOS app with Adapty SDK." displayed_sidebar: sdkios --- This page contains guides for displaying legacy Paywall Builder paywalls in your iOS app. Choose the topic you need: - **[Fetch legacy Paywall Builder paywalls](ios-get-legacy-pb-paywalls)** - Retrieve legacy paywalls and their configuration - **[Present legacy Paywall Builder paywalls](ios-present-paywalls-legacy)** - Display legacy paywalls to users - **[Handle legacy paywall events](ios-handling-events-legacy)** - Manage legacy paywall interactions --- # File: ios-get-legacy-pb-paywalls.md --- --- title: "Fetch legacy Paywall Builder paywalls in iOS SDK" description: "Retrieve legacy PB paywalls in your iOS app with Adapty SDK." displayed_sidebar: sdkios --- 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! 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 should hardcode is the placement ID. Response parameters: | Parameter | Description | | :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](sdk-models#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-handling-events-legacy.md --- --- title: "Handle paywall events in legacy iOS SDK" description: "Handle events in iOS (Legacy) apps with Adapty’s event tracking system." toc_max_heading_level: 4 --- 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`. --- # File: ios-handling-events.md --- --- title: "Handle paywall events in iOS SDK" description: "Handle subscription-related events in iOS using Adapty for better app monetization." toc_max_heading_level: 4 --- :::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 `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. | ## 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, 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 `Adapty.makePurchase()` succeeds, this method will be invoked: ```swift showLineNumbers title="Swift" func paywallController( _ controller: AdaptyPaywallController, didFinishPurchase product: AdaptyPaywallProductWithoutDeterminingOffer, purchaseResult: AdaptyPurchaseResult ) { if !purchaseResult.isPurchaseCancelled { controller.dismiss(animated: true) } } ```
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 `Adapty.makePurchase()` fails, this method will be invoked: ```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 `Adapty.restorePurchases()` 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 `Adapty.restorePurchases()` 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-handling-onboarding-events.md --- --- title: "Handle onboarding events in iOS SDK" description: "Handle onboarding-related events in iOS using Adapty." toc_max_heading_level: 4 --- 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 } } ```
## Updating field state 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 saveUserPreference(elementId: action.elementId, value: params) case .multiSelect(let params): // Handle multiple selections saveUserPreferences(elementId: action.elementId, values: params) case .input(let params): // Handle text input saveUserInput(elementId: action.elementId, value: params) case .datePicker(let params): // Handle date selection saveUserDate(elementId: action.elementId, value: params) } } ``` :::note If you want to save or process data, you need to implement the methods yourself. ::: The `action` object contains: - `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, which can be one of the following types: - `select`: Single selection from a list of options. - `multiSelect`: Multiple selections from a list of options. - `input`: Text input from the user. - `datePicker`: Date selected by the user.
Saved data examples (Click to expand) ```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 } } } ```
## 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: ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, onPaywallAction action: AdaptyOnboardingsOpenPaywallAction) { 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 controller.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-implement-paywalls-manually.md --- --- title: "Implement paywalls manually in iOS SDK" description: "Learn how to implement paywalls manually in your iOS app with Adapty SDK." displayed_sidebar: sdkios --- This page contains guides for implementing paywalls manually in your iOS app. Choose the topic you need: - **[Fetch paywalls and products](fetch-paywalls-and-products)** - Retrieve paywalls and product data - **[Present remote config paywalls](present-remote-config-paywalls)** - Display remote config paywalls - **[Accept purchases](making-purchases)** - Handle purchase transactions - **[Restore purchases](restore-purchase)** - Restore previous purchases - **[Implement Observer mode](implement-observer-mode)** - Set up Observer mode for analytics and paywall integration - **[Report transactions in Observer Mode](report-transactions-observer-mode)** - Report purchase transactions in Observer Mode - **[Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode)** - Display Paywall Builder paywalls in Observer mode - **[Troubleshooting](ios-troubleshoot-purchases)** - Resolve common purchase issues --- # File: ios-legacy-install.md --- --- 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](display-pb-paywalls) and, if you're using paywalls not created with the Paywall Builder, [handle the purchase process](making-purchases) within your app. --- # File: ios-legacy.md --- --- title: "Legacy iOS SDK guides" description: "Legacy documentation for Adapty iOS SDK." displayed_sidebar: sdkios --- This page contains legacy documentation for Adapty iOS SDK. Choose the topic you need: - **[Legacy installation guide](ios-legacy-install)** - Install and configure legacy iOS SDK - **[Display legacy Paywall Builder paywalls](ios-display-legacy-pb-paywalls)** - Work with legacy paywall builder --- # File: ios-onboardings.md --- --- title: "Onboardings in iOS SDK" description: "Learn how to work with onboardings in your iOS app with Adapty SDK." displayed_sidebar: sdkios --- This page contains all guides for working with onboardings in your iOS app. Choose the topic you need: - **[Get onboardings](get-onboardings)** - Retrieve onboardings from Adapty - **[Display onboardings](ios-present-onboardings)** - Present onboardings to users - **[Handle onboarding events](ios-handling-onboarding-events)** - Manage onboarding interactions --- # File: ios-paywalls.md --- --- title: "Paywalls in iOS SDK" description: "Learn how to work with paywalls in your iOS app with Adapty SDK." displayed_sidebar: sdkios --- This page contains all guides for working with paywalls in your iOS app. Choose the topic you need: - **[Get paywalls](get-pb-paywalls)** - Retrieve paywalls from Adapty - **[Display paywalls](ios-present-paywalls)** - Present paywalls to users - **[Handle paywall events](ios-handling-events)** - Manage paywall interactions - **[Work with paywalls offline](ios-use-fallback-paywalls)** - Use fallback paywalls when offline - **[Localize paywalls](localizations-and-locale-codes)** - Support multiple languages - **[Implement web paywalls](ios-web-paywall)** - Use web-based paywalls - **[Implement paywalls manually](ios-implement-paywalls-manually)** - Build custom paywall UI --- # File: ios-present-onboardings.md --- --- 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 `AdaptyOnboarding` object containing all the onboarding properties. Use the [`AdaptyUI.getOnboarding`](get-onboardings) method. | | **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" // 0. Get an onboarding if you haven't done it yet let onboarding = try await Adapty.getOnboarding("YOUR_PLACEMENT_ID") // 1. Obtain the onboarding view configuration: let configuration = try AdaptyUI.getOnboardingConfiguration(forOnboarding: onboarding) // 2. Create Onboarding View Controller let onboardingController = AdaptyUI.onboardingController( configuration: 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 ) ``` --- # File: ios-present-paywall-builder-paywalls-in-observer-mode.md --- --- 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 } ``` 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. 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. 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" 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), [Flutter](sdk-installation-flutter#configure-adapty-sdk), [React Native](sdk-installation-reactnative#configure-adapty-sdks), 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" 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-present-paywalls-legacy.md --- --- 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-present-paywalls.md --- --- 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) ::: ## 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 // 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 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" 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: ios-quickstart-identify.md --- --- 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. 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). ## 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, created anonymous profiles won't affect the dashboard [analytics](analytics-charts.md), because installs will be counted by new device IDs. However, if you want to change this behavior and count new customer user IDs instead of device IDs, go to **App settings** and set up [**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**](testing-purchases-ios.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: ios-quickstart-paywalls.md --- --- 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](making-purchases). | | 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. ## 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) } ``` ```swift 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) } ``` ## 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. 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-reference.md --- --- title: "Reference for iOS SDK" description: "Reference documentation for Adapty iOS SDK." displayed_sidebar: sdkios --- This page contains reference documentation for Adapty iOS SDK. Choose the topic you need: - **[SDK models](sdk-models)** - Data models and structures used by the SDK - **[Handle errors](ios-sdk-error-handling)** - Error handling and troubleshooting - **[iOS SDK reference](https://swift.adapty.io/documentation/adapty/)** - Complete API documentation --- # File: ios-sdk-error-handling.md --- --- title: "Handle errors in iOS SDK" description: "Handle iOS SDK errors efficiently with Adapty’s troubleshooting guide." --- Adapty SDK has its own wrapper for all kinds of errors, called `AdaptyError`. Basically, every error returned by an SDK is `AdaptyError`. It has two useful properties: `originalError` and `adaptyErrorCode`, described below. **originalError** contains an original error in case you need the original one to work with. Can be [SKError](https://developer.apple.com/documentation/storekit/skerror), [NSError](https://developer.apple.com/documentation/foundation/nserror) or just general Swift [Error](https://developer.apple.com/documentation/swift/error). This property is optional since some errors can be generated directly by SDK, like inconsistent or missing data, and won't have the original error around which the wrapper was initially built. **adaptyErrorCode** can be used to handle common issues, like: - invalid credentials - network errors - cancelled payments - billing issues - invalid receipt - and much more It's pretty easy to check the error for specific codes and react to them accordingly. ```swift showLineNumbers title="Swift" do { let info = try await Adapty.makePurchase(product: product) } catch { if error.adaptyErrorCode == .paymentCancelled { // purchase was cancelled // you can offer discount to your user or remind them later } } ``` :::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. ::: ## StoreKit errors | Error | Code | Solution | |--------------------------------------------------------------------------------------------------------------------------------------------|------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [unknown](https://developer.apple.com/documentation/storekit/skerror/code/unknown) | 0 | Error code indicating that an unknown or unexpected error occurred.
Retry or see the [Other issues](#other-issues) section. | | [clientInvalid](https://developer.apple.com/documentation/storekit/skerror/code/clientinvalid) | 1 | This error code indicates that the client is not allowed to perform the attempted action. | | [paymentCancelled](https://developer.apple.com/documentation/storekit/skerror/code/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](https://developer.apple.com/documentation/storekit/skerror/code/paymentinvalid) | 3 | This error indicates that one of the payment parameters was not recognized by the App Store. | | [paymentNotAllowed](https://developer.apple.com/documentation/storekit/skerror/code/paymentnotallowed) | 4 | This error code indicates that the user is not allowed to authorize payments. | | [storeProductNotAvailable](https://developer.apple.com/documentation/storekit/skerror/code/storeproductnotavailable) | 5 | This error code indicates that the requested product is not available in the store.
Try re-installing the app. | | [cloudServicePermissionDenied](https://developer.apple.com/documentation/storekit/skerror/code/cloudservicepermissiondenied) | 6 | This error code indicates that the user has not allowed access to Cloud service information. | | [cloudServiceNetworkConnectionFailed](https://developer.apple.com/documentation/storekit/skerror/code/cloudservicenetworkconnectionfailed) | 7 | This error code indicates that the device could not connect to the network. | | [cloudServiceRevoked](https://developer.apple.com/documentation/storekit/skerror/code/cloudservicerevoked/) | 8 | This error code indicates that the user has revoked permission to use this cloud service. | | [privacyAcknowledgementRequired](https://developer.apple.com/documentation/storekit/skerror/code/privacyacknowledgementrequired) | 9 | This error code indicates that the user has not yet acknowledged Apple’s privacy policy. | | [unauthorizedRequestData](https://developer.apple.com/documentation/storekit/skerror/code/unauthorizedrequestdata) | 10 | This error code indicates that the app is attempting to use a property for which it does not have the required entitlement. | | [invalidOfferIdentifier](https://developer.apple.com/documentation/storekit/skerror/code/invalidofferidentifier) | 11 |

The offer [`identifier`](https://developer.apple.com/documentation/storekit/skpaymentdiscount/3043528-identifier) is not valid. For example, you have not set up an offer with that identifier in the App Store, or you have revoked the offer.

Make sure you set up desired offers in AppStore Connect and pass a valid offer identifier.

| | [invalidSignature](https://developer.apple.com/documentation/storekit/skerror/code/invalidsignature) | 12 | This error code indicates that the signature in a payment discount is not valid. | | [missingOfferParams](https://developer.apple.com/documentation/storekit/skerror/code/missingofferparams) | 13 | This error code indicates that parameters are missing in a payment discount. | | [invalidOfferPrice](https://developer.apple.com/documentation/storekit/skerror/code/invalidofferprice/) | 14 | This error code indicates that the price you specified in App Store Connect is no longer valid. Offers must always represent a discounted price. | | noProductIDsFound | 1000 |

This error 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, ignore it.

If you’re encountering this error, follow the steps in the [Fix for Code-1000 `noProductIDsFound` error](InvalidProductIdentifiers) section.

| | noProductsFound | 1001 | This error indicates that the product requested for purchase is not available in the store. | | productRequestFailed | 1002 | Unable to fetch available products at the moment. | | cantMakePayments | 1003 | In-app purchases are not allowed on this device. See the troubleshooting [guide](cantMakePayments). | | noPurchasesToRestore | 1004 | This error indicates that the App Store did not find the purchase to restore. | | [cantReadReceipt](https://developer.apple.com/documentation/storekit/skerror/code/paymentcancelled) | 1005 |

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

In the sandbox, you won't have a valid receipt file until you actually make a purchase, so make sure you do one before accessing it. During sandbox testing also make sure you signed in on a device with a valid Apple sandbox account.

| | productPurchaseFailed | 1006 | Product purchase failed. The StoreKit error unrelated to Adapty. Try using a new [sandbox profile](test-purchases-in-sandbox). If it doesn't help, contact the Apple support. | | missingOfferSigningParams | 1007 |

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.

| ## Network errors | Error | Code | Solution | | :------------- | :--- |:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | notActivated | 2002 | The Adapty SDK is not activated. You need to properly [configure Adapty SDK](sdk-installation-ios#configure-adapty-sdk) using the `Adapty.activate` method. | | badRequest | 2003 | Bad request.
Ensure you've completed all the steps required to [integrate with the App Store](app-store-connection-configuration). | | serverError | 2004 | Server error.
Try again after some time. If the issue is not resolved, contact the Adapty support team. | | networkFailed | 2005 | The error indicates issues with the network connection on the user's device.
Try disabling VPN or switching to WiFi from a cellular network or vice versa. | | decodingFailed | 2006 | This error indicates that response decoding failed.
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. | | encodingFailed | 2009 | This error indicates that request encoding failed. | | missingURL | 2010 | The requested URL is nil. | ## General errors | Error | Code | Solution | | :------------------- | :--- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | analyticsDisabled | 3000 | We can't handle analytics events, since you've [opted it out](analytics-integration#disabling-external-analytics-for-a-specific-customer). | | wrongParam | 3001 | This error indicates that some of your parameters are not correct.
If you're using the Adapty paywall builder and can't display a paywall because of this error, toggle on **Show on device** in the paywall builder.
Another possible reason for this issue is that the local [fallback](fallback-paywalls) file version doesn't match the SDK version. Download a new file in the dashboard. | | activateOnceError | 3005 | It is not possible to call `.activate` method more than once. | | profileWasChanged | 3006 | The user profile was changed during the operation.
This error can occur when you call `identify`, and then call another method before `identify` succeds. To avoid it, wait untill `identify` suceeds before calling other methods. | | unsupportedData | 3007 | This error indicates that the data format is not supported by the SDK. | | persistingDataError | 3100 | It was an error while saving data. | | fetchTimeoutError | 3101 | This error indicates that the fetch operation timed out. | | operationInterrupted | 9000 | This operation was interrupted by the system. | ## 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 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-ios#logging) and share logs with the team. You can also attach relevant code snippets. --- # File: ios-sdk-migration-guides.md --- --- title: "iOS SDK Migration Guides" description: "Migration guides for Adapty iOS SDK versions." --- This page contains all migration guides for Adapty iOS SDK. Choose the version you want to migrate to for detailed instructions: - **[Migrate to v. 3.4](migration-to-ios-sdk-34)** - **[Migrate to v. 3.3](migration-to-ios330)** - **[Migrate to v. 3.0](migration-to-ios-sdk-v3)** --- # File: ios-sdk-overview.md --- --- title: "iOS SDK overview" description: "Learn about Adapty iOS SDK and its key features." slug: /ios-sdk-overview displayed_sidebar: sdkios --- [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-iOS.svg?style=flat&logo=apple)](https://github.com/adaptyteam/AdaptySDK-iOS/releases) Welcome! We're here to make in-app purchases a breeze 🚀 We've built the Adapty iOS 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 App Store Connect 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.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-ios.md): Add the SDK as a dependency to your project and activate it in the code. 2. [Enable purchases through paywalls](ios-quickstart-paywalls.md): Set up the purchase flow so users can buy products. 3. [Check the subscription status](ios-check-subscription-status.md): Automatically check the user's subscription state and control their access to paid content. 4. [Identify users (optional)](ios-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](https://github.com/adaptyteam/AdaptySDK-iOS/tree/master/Examples) that demonstrate the full setup - **Video tutorials**: Follow along with our step-by-step implementation videos 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: ios-settings.md --- --- title: "Apple App Store credentials" description: "Configure iOS settings in Adapty for seamless subscription management." --- To configure the App Store credentials and ensure optimal functionality of the Adapty iOS SDK, navigate to the [iOS SDK](https://app.adapty.io/settings/ios-sdk) tab within the App Settings page of the Adapty Dashboard. Then, configure the following parameters: | Field | Description | |-----|-----------| | **Bundle ID** | Your [app bundle ID](app-store-connection-configuration#step-1-provide-bundle-id). | | **In-app purchase API (StoreKit 2)** | [Keys](app-store-connection-configuration#step-2-provide-issuer-id-and-key-id) to enable secure authentication and validation of in-app purchase transaction history requests. | | **App Store Server Notifications** | URL that is used to enable [server2server notifications](enable-app-store-server-notifications) from the App Store to monitor and respond to users' subscription status changes | | **App Store Promotional Offers** | Subscription keys for creating [Promotional offers](generate-in-app-purchase-key) in Adapty for specific products. | | **App Store Connect shared secret (LEGACY)** |

**Legacy key for Adapty SDK prior to v.2.9.0**

[A key](app-store-connection-configuration#step-4-enter-app-store-shared-secret) for receipts validation and preventing fraud in your app.

| --- # File: ios-test.md --- --- title: "Test & release in iOS SDK" description: "Learn how to check subscription status in your iOS app with Adapty." displayed_sidebar: sdkios --- 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. For comprehensive testing of your in-app purchases, including sandbox testing and TestFlight validation, see our [testing guide](testing-purchases-ios.md). --- # File: ios-troubleshoot-paywall-builder.md --- --- 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-troubleshoot-purchases.md --- --- 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: ios-use-fallback-paywalls.md --- --- title: "Use fallbacks in iOS SDK" description: "Learn how to use fallback paywalls and onboardings on iOS to ensure seamless user experiences." --- To use fallback paywalls and onboardings: 1. In Xcode, use the menu **File** -> **Add Files to "YourProjectName"** to add the fallback JSON file you [downloaded in the Adapty Dashboard](fallback-paywalls#download-fallback-paywalls-as-a-file-in-the-adapty-dashboard) to your project bundle. 2. Call the `.setFallback` method. Place this method in your code **before** fetching a paywall or onboarding, ensuring that the mobile app possesses it when a fallback paywall or onboarding is required to replace the standard one. Here's an example of retrieving fallback paywall or onboarding data from a locally stored JSON file named `ios_fallback.json`. ```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 file with fallback paywalls and onboardings you [downloaded in the Adapty Dashboard](fallback-paywalls#download-fallback-paywalls-as-a-file-in-the-adapty-dashboard). | --- # File: ios-user.md --- --- title: "Users & access in iOS SDK" description: "Learn how to work with users and access levels in your iOS app with Adapty SDK." displayed_sidebar: sdkios --- This page contains all guides for working with users and access levels in your iOS app. Choose the topic you need: - **[Identify users](identifying-users)** - Learn how to identify users in your app - **[Update user data](setting-user-attributes)** - Set user attributes and profile data - **[Listen for subscription status changes](subscription-status)** - Monitor subscription changes in real-time - **[Deal with App Tracking Transparency (ATT)](ios-deal-with-att)** - Handle ATT requirements - **[Kids Mode](kids-mode)** - Implement Kids Mode for your app --- # File: ios-web-paywall.md --- --- title: "Implement web paywalls in iOS SDK" description: "Set up a web paywall to get paid without the App Store fees and audits." sidebar_label: "Implement web paywalls" --- :::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. ```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: product) } 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. ::: --- # File: kids-mode-android.md --- --- 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: kids-mode-flutter.md --- --- 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/app-store/kids-apps/) 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: kids-mode-react-native.md --- --- title: "Kids Mode in React Native SDK" description: "Easily enable Kids Mode to comply with Apple and Google policies. No IDFA, GAID, or ad data collected in React Native SDK." --- If your React Native application is intended for kids, you must follow the policies of [Apple](https://developer.apple.com/app-store/kids-apps/) 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 Support for Kids Mode in React Native is coming soon! For now, you can follow the native platform guides: - [Kids Mode in iOS SDK](kids-mode) for iOS configuration - [Kids Mode in Android SDK](kids-mode-android) for Android configuration --- # File: kids-mode-unity.md --- --- title: "Kids Mode in Unity SDK" description: "Easily enable Kids Mode to comply with Apple and Google policies. No IDFA, GAID, or ad data collected in Unity SDK." --- If your Unity application is intended for kids, you must follow the policies of [Apple](https://developer.apple.com/app-store/kids-apps/) 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 Support for Kids Mode in Unity is coming soon! For now, you can follow the native platform guides: - [Kids Mode in iOS SDK](kids-mode) for iOS configuration - [Kids Mode in Android SDK](kids-mode-android) for Android configuration --- # File: kids-mode.md --- --- 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/app-store/kids-apps/). 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 You can only enable the Kids Mode with Cocoa Pods. In order to comply with policies, disable the collection of the user's IDFA and IP address: 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: local-fallback-onboarding.md --- --- title: "Download fallback onboardings" description: "Use local fallback onboardings in Adapty to ensure seamless subscription flows." --- Typically, [onboardings](onboardings.md) are fetched from the server when a customer accesses them, and you need to get their configuration before accessing them. However, if you can't get an onboarding config URL at the moment, you can use fallbacks to get the onboarding config URL and other data. Adapty generates fallbacks as a JSON file in the necessary format, reflecting the default versions of the paywalls and onboardings you've configured in the Adapty Dashboard. To use the file, download it - one per app store, place it alongside your app on the user's device, and pass its contents to the `.setFallback` method, following the instructions outlined below. :::important Note that onboarding fallbacks won't let your users access the onboarding if they don't have an Internet connection. Onboarding fallbacks are primarily URLs you can use to download onboarding configs. Even if you use onboarding configs, handle the [onboarding offline mode](onboarding-offline.md) in your app. :::
Before you start adding local fallback paywalls and onboardings (Click to Expand) 1. Create [onboardings](onboardings.md). 2. [Create placements and add paywalls and onboardings to the placements](create-placement). Placement is the location where the paywall/onboarding will be shown.
The JSON file with fallbacks contains both paywalls and onboardings at once. To download the JSON file, open **[Placements](https://app.adapty.io/placements)** in the Adapty main menu. Click the **Fallbacks** button. You will get the JSON file. Use its contents in the [iOS](ios-use-fallback-paywalls), [Android](android-use-fallback-paywalls), [React Native](react-native-use-fallback-paywalls), [Flutter](flutter-use-fallback-paywalls), and [Unity](unity-use-fallback-paywalls) method in your mobile app code.
Onboarding fallback example (Click to Expand) ``` "PLACEMENT_ID": {"data": [{"variation_id":"cb1c0ef8-aecd-4a53-a6f3-b98266e66884", "onboarding_id":"daf25858-3fa2-4981-8500-9c8a30e5b7e6", "onboarding_name":"ONBOARDING_NAME", "onboarding_builder": {"config_url":"CONFIG_URL", "lang":"en"}, "remote_config":null, "cross_placement_info":null, "weight":100}], "meta": {"placement": {"developer_id":"DEVELOPER_ID", "is_tracking_purchases":true, "audience_name":"All Users", "placement_audience_version_id":"a9eb3ab8-3178-477d-84d4-ef9d3978e48b", "revision":0, "ab_test_name":"A/B_TEST_NAME" } } }, ```
--- # File: local-fallback-paywalls.md --- --- title: "Download fallback paywalls" description: "Use local fallback paywalls in Adapty to ensure seamless subscription flows." --- Typically, [paywalls](paywalls.md) are fetched from the server when a customer accesses them. Adapty allows you to define fallback paywalls and onboardings for the situations when a user opens the app without a connection to the Adapty backend (e.g., no internet connection or in the rare case of backend unavailability) and there's no cache on the device. Adapty generates fallbacks as a JSON file in the necessary format, reflecting the default (English) versions of the paywalls you've configured in the Adapty Dashboard. Simply download the file - one per app store, place it alongside your app on the user's device, and pass its contents to the `.setFallback` method, following the instructions outlined below.
Before you start adding local fallback paywalls and onboardings (Click to Expand) 1. [Create products](create-product) you want to sell. 2. Create [paywalls]. 3. [Create placements and add paywalls to the placements](create-placement). Placement is the location where the paywall will be shown.
The JSON file with fallbacks contains both paywalls and onboardings at once. To download the JSON file, open **[Placements](https://app.adapty.io/placements)** in the Adapty main menu. Click the **Fallbacks** button. You will get the JSON file. Use its contents in the [iOS](ios-use-fallback-paywalls), [Android](android-use-fallback-paywalls), [React Native](react-native-use-fallback-paywalls), [Flutter](flutter-use-fallback-paywalls), and [Unity](unity-use-fallback-paywalls) method in your mobile app code. --- # File: localizations-and-locale-codes.md --- --- 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: localize-onboardings.md --- --- title: "Localize onboardings" description: "Manage onboarding localizations to reach a global audience." --- :::important Localizations in onboardings are supported in the [iOS](get-onboardings.md) and [Android](android-get-onboardings.md) SDKs starting from SDK version 3.11. If your SDK version is below 3.11, the default locale will be used. ::: Localization can be a time-consuming process that requires careful attention to detail. When you use Onboarding Builder, Adapty handles most of the heavy lifting for you. This page explains how the localization process works. Once you've finished configuring your onboarding in the default language and are satisfied with the result, you're ready to add support for additional languages. ## Add and set up localization 1. In the Onboarding Builder, click the globe icon in the top-right and select **Add locale**. 2. Choose **Language**. 3. **Optional**: Enable **Translate with AI** to automatically translate all content from the original onboarding into the selected language. 4. Click **Create**. Now, you can translate the content manually, use AI, or export the localization file for external translators. Adapty will create a copy of your onboarding where you can modify both the text content and visual elements to suit your target audience. :::tip You can customize the design of each locale (images, colors, layouts) to better target different audiences. However, structural changes (adding or removing screens and elements) can only be made in the default locale. ::: ## Translate onboardings using AI AI-powered translation provides a quick and efficient way to localize your onboarding. Translation typically takes 1-2 minutes depending on the onboarding size. You can use AI translation at two different stages: - **When adding a new locale:** Check **Translate with AI** during locale creation to generate a pre-translated version. - **After creating a locale:** Navigate to the globe icon → **Manage locales**, then click **Translate with AI** next to your desired locale. :::important Any manual edits you've made to the localized version will be overwritten. Additionally, changes made to the default locale after AI translation won't be automatically reflected in translated versions – you'll need to re-run the translation. ::: ## Export localization files for external translation You can export localization files to share with your translators and then import the translated results back into Adapty. Exporting by the **Export** button creates one `.tsv` file for all languages. ## Import files Once you’ve received the translated file, use the **Import** button to upload it. Adapty will automatically validate the files to ensure they match the correct format and paywall configuration structure. :::tip If you send this file to several translators at the same time, remember to remove extra columns from the file when importing. Otherwise, some translations will be overwritten by unchanged columns. ::: ### Import file format To ensure a successful import, the import file must meet the following requirements: - **File extension:** The file must have a `.tsv` extension. - **Only tabulations as separators**: Use tabulations as separators. Other separators will result in errors. - **Header line**: The file must include a header line. - **Correct column names:** The column names must be **Key** and full localization names. - **Correct Key names**: Values in the **Key** column must remain unchanged, as they correspond to screen and element identifiers.. - **No additional entities:** Ensure the file doesn’t include entities not present in the current onboarding configuration. Extra entities will result in errors. - **Partial import:** The file can include all or just some entities from the current paywall configuration. --- # File: ltv.md --- --- title: "Lifetime Value (LTV)" description: "Learn how to calculate and optimize Lifetime Value (LTV) in Adapty." --- The Realized LTV (Lifetime value) per paying customer displays the revenue that a paying customer cohort actually generated after refunds have been deducted, divided by the number of paying customers in that cohort. Therefore, this chart tells you how much revenue you generate on average from each paying customer. Adapty designs the LTV chart to answer several important questions about your app's revenue and customer behavior such as: 1. How much money does each cohort bring in over their lifetime with your app? 2. At what point in time does a cohort pay off? 3. How can you optimize your app's marketing and acquisition spend to attract valuable, high LTV customers? 4. How long does it take to recoup your investment in acquiring new customers? The LTV chart works with the app data we gather through our SDK and in-app events. With this information, you will be able to gain insights into how your subscriptions are performing and how much revenue is generated from your subscribers during a given period of time. You can use this information to make informed decisions about your subscription offerings, ad spending, and customer acquisition strategies. Additionally, the filters will allow you to segment the data by country, attribution, and other variables, giving you a more granular understanding of your customer base. ### LTV by renewals The **LTV by renewals** view presents data pertaining to the subscription period (P), specifically capturing the first instance when a customer makes a payment. In the case of a weekly subscription, this corresponds to the subsequent weekly subscription period. ### LTV by days The **LTV by days** view organizes and filters data based on daily, weekly, or monthly intervals. It provides insights into the total revenue generated by all users who installed the app on a specific day, week, or month, divided by the number of paying users during that same period. This view offers valuable insights into revenue tracking and enables a comprehensive understanding of user behavior over time. ### Calculation Realized LTV is calculated using the total revenue generated from each customer cohort, minus refunds. _LTV for the day/week/month = Revenue gained from all the paying users who installed the app on this day/week/month / the number of paying users who installed the app on this day/week/month_ The LTV calculation includes upgrades, downgrades, and reactivations, such as when a user changes their subscription plan or pricing. It takes into account the revenue generated from the initial subscription and subsequent renewals based on the updated plan. ### LTV chart usage The LTV chart is a valuable tool in Adapty that provides insights into the long-term value of your customers. By analyzing customer behavior and purchase patterns over time, the LTV chart helps you understand the revenue generated by different customer segments or cohorts. The LTV chart is particularly useful for identifying high-value customer segments, tracking the effectiveness of marketing campaigns, and evaluating the overall financial performance of your business. By understanding the lifetime value of your customers, you can make informed decisions about resource allocation, customer acquisition strategies, and customer retention initiatives. In addition, the LTV chart can be used to compare different customer segments, assess the impact of product changes or pricing adjustments, and identify opportunities for upselling or cross-selling. ### Available grouping and filtering Both filters and groupings can be applied to both the renewals and days views of the LTV chart, allowing you to drill down into specific cohorts and understand their behavior over time - ✅ Filter by: Attribution, country, paywall, store, product, and duration. - ✅ Group by: Product, country, store, duration, and by cohort day, week, month, and year. You can find more information about the available controls, filters, grouping options, tax and commission controls, and how to use them in [this documentation.](controls-filters-grouping-compare-proceeds) The Realized LTV chart in Adapty helps to gain valuable insights into customer behavior, optimize marketing strategies, track revenue performance, and make data-driven decisions to maximize the long-term value of their customers. --- # File: making-purchases.md --- --- 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 In paywalls built with [Paywall Builder](adapty-paywall-builder) purchases are processed automatically with no additional code. If that's your case — you can skip this step. ::: ```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`](sdk-models#adaptypaywallproduct) object retrieved from the paywall. | Response parameters: | Parameter | Description | |---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Profile** |

If the request has been successful, the response contains this object. An [AdaptyProfile](sdk-models#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: manage-paywall-ui-elements.md --- --- title: "Manage paywall UI elements" description: "Customize and manage paywall UI elements to enhance user experience." --- After choosing a template, the elements of it will be displayed in the left pane. Use this pane to organize the elements on your paywall. The elements will appear on the paywall in the same order as they do in the left pane. :::warning This section describes the new Paywall Builder, which works with iOS, Android, and React Native SDKs version 3.0 or higher, and Flutter and Unity SDKs version 3.3.0 or higher. For information on the legacy Paywall Builder compatible with Adapty SDK v2.x or earlier, see [Legacy Paywall Builder](adapty-paywall-builder-legacy). ::: ## Add element To add an element to your paywall above the layout: 1. Click the **Add Element** button in the left pane. 2. Choose the element you want to add. The new element will appear in the list above the **Footer**. To add an element to a compound element: 1. Click the **Plus** button next to the compound element. 2. Select the element you want to add. ## Rename paywall element To rename an element: 1. Click the element in the left pane to open its details. 2. Click the ellipse button in the right pane and choose the **Edit** option. 3. Type the new name of the element and press **Enter**. ## Duplicate element To duplicate an element : 1. Click the element in the left pane to open its details. 2. Click the ellipse button in the right pane and choose the **Duplicate** option. The duplicated element, with "Copy" added to its name, will appear in the left pane as a complete duplicate of the original. ## Move element To move an element: Drag and drop the element to its new position on the layout or within a compound element. A purple line indicates an available position for the element, while a red line shows an inaccessible position. ## Hide / show element Even though you have already created and configured an element, you can temporarily hide it from the paywall. That is convenient if you plan to add it later without losing all the configuration you made. After hiding an element, the paywall looks as though the element was never added, all alignments and spaces are recalculated and redrawn. To hide an element: 1. Click the element in the left pane to open its details. 2. Click the ellipse button in the right pane and choose the **Hide** option. The hidden element is marked in both the main pane - as a note and in the left pane if you choose it. To show the element again, click the ellipse button in the right pane and choose the **Show** option. ## Delete element To delete an element from the paywall: 1. Click the element in the left pane to open its details. 2. Click the ellipse button in the right pane and choose the **Delete** option. --- # File: maths-behind-it.md --- --- title: "Maths behind the A/B tests" description: "Understand the math behind subscription analytics for better revenue insights." --- A/B testing is a powerful technique used to compare the performance of two different versions of a paywall or onboarding. The ultimate goal is to determine which version is more effective based on the average revenue per user over a 12-month period. However, waiting for a full year to collect data and make decisions is impractical. Therefore, a 2-week revenue per user is used as a proxy metric, chosen based on historical data analysis to approximate the target metric. To achieve accurate and reliable results, it is crucial to employ a robust statistical method capable of handling diverse data types. Bayesian statistics, a popular approach in modern data analysis, provides a flexible and intuitive framework for A/B testing. By incorporating prior knowledge and updating it with new data, Bayesian methods allow for better decision-making under uncertainty. This document provides a comprehensive guide to the mathematical analysis employed by Adapty in evaluating A/B test results and providing valuable insights for data-driven decision-making. ## Adapty's approach to statistical analysis Adapty employs a comprehensive approach to statistical analysis in order to evaluate the performance of A/B tests and provide accurate and reliable insights. Our methodology consists of the following key steps: 1. **Metric definition:** To conduct an AB test successfully, you need to identify and define the key metric that aligns with the specific goals and objectives of the analysis. Adapty leveraged a huge amount of historical subscription app data to determine which fits the role of a proxy metric for the long-term goal of average revenue after 1 year - and it is ARPU after 14 days. 2. **Hypothesis formulation:** We create two hypotheses for the A/B test. The null hypothesis (H0) assumes that there is no significant difference between the control group (A) and the test group (B). The alternative hypothesis (H1) suggests that there is a significant difference between the two or more groups. 3. **Distribution selection:** We choose the best distribution family based on the data characteristics and the metric we observe. The most frequent choice here is log-normal distribution (taking into account zero values). 4. **Probability-to-be-best calculation:** Utilising the Bayesian approach to A/B testing, we calculate the probability to be the best option for every paywall or onboarding variant participating in the test. This value is surely connected to the p-values we used before, but it is essentially a different approach, more robust, and easier to understand. 5. **Results interpretation:** Probability to be best is exactly how it sounds. The larger the probability is, the higher the likelihood of a specific option being the best choice for the task. You need to determine the threshold for decision-making yourself, it should depend on many other factors of your specific situation, but a common probability choice is 95%. 6. **Prediction intervals:** Adapty calculates prediction intervals for the performance metrics of each group, providing a range of values within which the true population parameter is likely to fall. This helps quantify the uncertainty associated with the estimated performance metrics. ## Sample size determination Determining an appropriate sample size is crucial for reliable and conclusive A/B test results. Adapty considers factors such as the statistical power and expected effect size, which continue to be important even under the Bayesian approach, to ensure an adequate sample size. The methods for estimating the required sample size, specific to the Bayesian approach we now employ, ensure the reliability of the analysis. To learn more about the functionality of A/B tests, we recommend referring to our documentation on [creating](ab-tests) and [running A/B tests](run_stop_ab_tests), as well as understanding the various [A/B test metrics and results.](results-and-metrics). Adapty's analytical framework for A/B tests now employs a Bayesian approach, but the focus remains on the definition of metrics, formulation of hypotheses, and the selection of distributions. However, instead of determining p-values, we now compute the posterior distributions and calculate the probability of each variant being the best. We also determine the prediction intervals now. This revised approach, while still comprehensive and even more robust, is designed to provide insights that are more intuitive and easier to interpret. The goal remains to empower businesses to optimize their strategies, improve performance, and drive growth based on a robust statistical analysis of their A/B tests. --- # File: members-settings.md --- --- title: "Members" description: "Manage member settings and permissions in Adapty’s dashboard." --- :::note This page is about Adapty dashboard members If you want to give different access levels to users of your app check [Access Level.](access-level) ::: The Adapty dashboard members system allows you to grant different levels of access to Adapty and specify applications for each member. ### Roles The following roles are available for members in the Adapty dashboard: **Owner:** The Owner is the original creator of the Adapty account and holds the highest level of access and control. Owners have complete access to Adapty billing, allowing them to manage payment information and subscription plans. Additionally, only Owners and Admins can specify application access for new members. There can be only one Owner for each Adapty account. **Admin:** Members with the Admin role have full access to the chosen applications. They can perform various management tasks, including creating and modifying paywalls, conducting A/B tests, analyzing analytics, and managing members within those applications. **Viewer:** Members with the Viewer role have read-only access to the chosen applications. They can view information but cannot create or modify paywalls, A/B tests, and other features, invite new users, create new apps, and change the app settings. **Support:** Members with the Support role have access only to user profiles in chosen applications. However, they cannot perform actions like adding new members or accessing any other sections of Adapty. This role is particularly suitable for support teams or individuals who need to assist customers with subscription-related inquiries or troubleshooting. ### How to add a member To access the members section and add new members, please navigate to the [Account section](https://app.adapty.io/account) in the Adapty dashboard. Within this section, you have the ability to select roles and specify apps for the new members, provided you have sufficient rights. :::note It is only possible to invite an email that is not yet registered in Adapty. If your colleague already created a standalone account, invite another email address of theirs or contact Adapty support - we'll delete the problematic account. ::: If you want to transfer ownership of Adapty account, contact support. By following these steps and utilizing the information provided, you can effectively manage member access and permissions within your Adapty account using the Adapty dashboard members system. For details on the number of members allowed for each plan, please refer to our [Pricing documentation.](https://adapty.io/pricing/) --- # File: messaging.md --- --- title: "Messaging service integrations" description: "Use Adapty’s messaging tools to improve subscription engagement and retention." --- The acquisition is not easy or cheap in the growing mobile market. So wisely treating attracted users improves your unit economy, especially in highly competitive niches. Adapty provides real-time information about core users' payment actions. We know when your customer took a trial, if he had troubles with his payment, or if he purchased a subscription and decided to cancel later. All these and other events show the change in the state of the customer. And this is the best moment to react - send an offer, or personal gift, or whatever retaining. Push notification platforms allow describing a user with standard and custom tags to build an effective automatic system of retention. To make this system work you just need trigger events to let the system know that it's time to send a message. These events will come to the push platform from Adapty through the set integration. Please choose below the service that you need to integrate and follow the instructions: - [Braze](braze) - [OneSignal](onesignal) - [Pushwoosh](pushwoosh) - [Slack](slack) :::note Don't see your attribution provider? Let us know! [Write to the Adapty support](mailto:support@adapty.io) and we'll consider adding it. ::: ## Event properties Webhook events are sent in JSON format. All events follow the same structure, but their fields vary based on the event type, store, and your specific configuration. | Property | Type | Description | | ----------------------------- | ------------- | ------------------------------------------------------------ | | **profile_id** | uuid | Adapty user ID. | | **currency** | str | Local currency (defaults to USD). | | **price_usd** | float | Product price before Apple/Google cut. Revenue. | | **proceeds_usd** | float | Product price after Apple/Google cut. Net revenue. | | **net_revenue_usd** | float | Net revenue (income after Apple/Google cut and taxes) in USD. Can be empty. | | **price_local** | float | Product price before Apple/Google cut in local currency. Revenue. | | **proceeds_local** | float | Product price after Apple/Google cut in local currency. Net revenue. | | **transaction_id** | str | A unique identifier for a transaction such as a purchase or renewal. | | **original_transaction_id** | str | The transaction identifier of the original purchase. | | **purchase_date** | ISO 8601 date | The date and time of product purchase. | | **original_purchase_date** | ISO 8601 date | The date and time of the original purchase. | | **environment** | str | Can be _Sandbox_ or _Production_. | | **vendor_product_id** | str | Product ID in the Apple App Store, Google Play Store, or Stripe. | | **base_plan_id** | str | [Base plan ID](https://support.google.com/googleplay/android-developer/answer/12154973) in the Google Play Store or [price ID](https://docs.stripe.com/products-prices/how-products-and-prices-work#what-is-a-price) in Stripe. | | **event_datetime** | ISO 8601 date | The date and time of the event. | | **store** | str | Can be _app_store_ or _play_store_. | | **trial_duration** | str | Duration of a trial period in days. Sent in a format "{} days" , for example, "7 days". | | **cancellation_reason** | str |

A reason why the user canceled a subscription.

Can be

iOS & Android

_voluntarily_cancelled_, _billing_error_, _refund_

iOS

_price_increase_, _product_was_not_available_, _unknown_

Android

_new_subscription_replace_, _cancelled_by_developer_

| | **subscription_expires_at** | ISO 8601 date | The Expiration date of subscription. Usually in the future. | | **consecutive_payments** | int | The number of periods, that a user is subscribed to without interruptions. Includes the current period. | | **rate_after_first_year** | bool | Boolean indicates that a vendor reduces cuts to 15%. Apple and Google have 30% first-year cut and 15% after it. | | **promotional_offer_id** | str | ID of promotional offer as indicated in the Product section of the Adapty Dashboard | | **store_offer_category** | str | Can be _introductory_ or _promotional_. | | **store_offer_discount_type** | str | Can be _free_trial_, _pay_as_you_go_ or _pay_up_front_. | | **paywall_name** | str | Name of the paywall where the transaction originated. | | **paywall_revision** | int | Revision of the paywall where the transaction originated. The value is set to 1. | | **developer_id** | str | Developer (SDK) ID of the placement where the transaction originated. | | **ab_test_name** | str | Name of the A/B test where the transaction originated. | | **ab_test_revision** | int | Revision of the A/B test where the transaction originated. The value is set to 1. | | **cohort_name** | str | Name of the audience to which the profile belongs to. | | **profile_event_id** | uuid | Unique event ID that can be used for deduplication. | | **store_country** | str | The country sent to us by the store. | | **profile_ip_address** | str | Profile IP (can be IPv4 or IPv6, with IPv4 preferred when available). It is updated each time IP of the device changes. | | **profile_country** | str | Determined by Adapty, based on profile IP. | | **profile_total_revenue_usd** | float | Total revenue for the profile, refunds included. | | **variation_id** | uuid | Unique ID of the paywall where the purchase was made. | | **access_level_id** | str | Paid access level ID | | **is_active** | bool | Boolean indicating whether paid access level is active for the profile. | | **will_renew** | bool | Boolean indicating whether paid access level will be renewed. | | **is_refund** | bool | Boolean indicating whether transaction is refunded. | | **is_lifetime** | bool | Boolean indicating whether paid access level is lifetime. | | **is_in_grace_period** | bool | Boolean indicating whether profile is in grace period. | | **starts_at** | ISO 8601 date | Date and time when paid access level starts for the user. | | **renewed_at** | ISO 8601 date | Date and time when paid access will be renewed. | | **expires_at** | ISO 8601 date | Date and time when paid access will expire. | | **activated_at** | ISO 8601 date | Date and time when paid access was activated. | | **billing_issue_detected_at** | ISO 8601 date | Date and time of billing issue. | | **profile_has_access_level** | Bool | A boolean that indicates whether the profile has an active access level (Webhook only). | Each event has the following properties: `transaction_id, original_transaction_id, purchase_date, original_purchase_date, environment, vendor_product_id, event_datetime, store`. In addition, some events have additional properties. For the events `subscription_refunded` and `non_subscription_purchase_refunded`, it is mandatory to provide the values of `price_usd` and `proceeds_usd` as additional properties. | Event Name | Properties | | :---------------------------------- | :----------------------------------------------------------- | | **subscription\_initial\_purchase** | price\_usd, proceeds\_usd, subscription\_expires\_at, consecutive\_payments, rate\_after\_first\_year, trial\_duration | | **subscription\_renewed** | price\_usd, proceeds\_usd, subscription\_expires\_at, consecutive\_payments, rate\_after\_first\_year, trial\_duration | | **subscription\_cancelled** | cancellation\_reason, trial\_duration | | **trial\_started** | subscription\_expires\_at, trial\_duration | | **trial\_converted** | price\_usd, proceeds\_usd, subscription\_expires\_at, consecutive\_payments, rate\_after\_first\_year, trial\_duration | | **trial\_cancelled** | cancellation\_reason, trial\_duration | | **non\_subscription\_purchase** | price\_usd, proceeds\_usd | | **billing\_issue\_detected** | subscription\_expires\_at, trial\_duration | | **entered\_grace\_period** | subscription\_expires\_at, trial\_duration | Event example ```json title="Json" { "price_usd": 9.99, "proceeds_usd": 6.99, "transaction_id": "1000000628581600", "original_transaction_id": "1000000628581600", "purchase_date": "2020-02-18T18:40:22.000000+0000", "original_purchase_date": "2020-02-18T18:40:22.000000+0000", "environment": "Sandbox", "vendor_product_id": "premium", "event_datetime": "2020-02-18T18:40:22.000000+0000", "store": "app_store" } ``` Adapty sends events to your server and 3rd party analytical systems. **profile_ip_address** property is synchronized with the current device IP. Each time the Adapty servers receive info from the SDK, the IP will be updated if it differs from the one we have on record. --- # File: migrate-paywalls.md --- --- title: "Migrate paywalls between apps" description: "Learn how to migrate paywalls from other apps in Adapty." --- With Adapty, you don't need to build a new paywall from scratch for every app. If you manage multiple apps, you can migrate the paywall builder configuration of any paywall created with the builder from one app to another. Migration lets you copy all visual configurations: - Layout settings for paywall and all paywall elements - Media - Localization Migration applies only to builder configuration and it doesn't copy products or remote config. :::note If you migrate a paywall builder configuration with custom fonts, test them on a device as they may display incorrectly. ::: ## Migrate paywall To migrate a paywall builder configuration: 1. **For new paywall**: Start [paywall creation](create-paywall.md) and add products. **For existing paywall**: Go to the **Layout settings** section of the **Builder & Generator** tab. 2. Click **Migrate** when editing the paywall template. 3. Select the app and the paywall you want to copy the configuration from. 4. Click **Copy Selected Paywall**. After migration, you can make any edits you need and they won't affect the original paywall. --- # File: migrate-to-adapty-from-another-solutions.md --- --- title: "Migrate to Adapty" description: "Migrate to Adapty from other subscription management solutions easily." --- Migration has three steps: 1. Switching to Adapty SDK. 2. Changing [Apple](enable-app-store-server-notifications)/ [Google](enable-real-time-developer-notifications-rtdn) server2server notifications webhook. 3. (Optional) [Importing historical data to Adapty](importing-historical-data-to-adapty) to instantly pull statistics. Let's quickly go through each part. :::info Your subscribers will migrate automatically All users who have ever activated subscription will move as soon as they open a new version with Adapty SDK. The subscription status validation and premium access will be restored automatically. ::: ### Installing Adapty SDK Install Adapty SDK for your platform ([iOS](sdk-installation-ios), [Android](sdk-installation-android), [React Native](sdk-installation-reactnative), [Flutter](sdk-installation-flutter), [Unity](sdk-installation-unity)) in your app and replace your legacy logic with appropriate methods from Adapty SDK. Core things you need to replace: - Checking an [Access level](access-level) to open a gated content; - Making a purchase; - Restoring purchase; - Getting/setting information about your user. :::tip Switching from another subscription provider? Follow our guide for a detailed walk-though: - [Migration from RevenueCat](migration-from-revenuecat) (20 minutes) ::: ### Changing Apple server notifications Apple and Google send us events that happen with users' subscriptions outside of the application (renewal, cancellation, pausing, refund, etc.) via [App Store server notifications](enable-app-store-server-notifications). Adapty can work without this URL, but you'll get a limited feature set. For example, [Integrations](events) to 3rd party services will be delayed, subscription analytics won't be in real-time, and paywall A/B testing metrics won't be accurate. When switching from a legacy system, sometimes you want two systems to work simultaneously for some time. In that case, you can use our [raw events forwarding](enable-app-store-server-notifications#raw-events-forwarding), where Adapty is a proxy server for your legacy system. ### Move historical data to Adapty Moving historical data is optional and won't affect your subscribers' state. However, there are a number of reasons why it's better to do so: 1. **Analytics will work correctly instantly**. Adapty matches subscribers by original transaction ID, and we don't count events from Apple webhook without exposing them to Adapty SDK (we technically can't do it). 2. **Used data will be there**. You'll have all Adapty profiles with user properties and can use them in [Segments](segments), and [Profiles/CRM](profiles-crm). Follow our [tutorial](importing-historical-data-to-adapty) to send us historical data. --- # File: migration-from-glassfy.md --- --- title: "Migration from Glassfy" description: "Migrate from Glassfy to Adapty seamlessly and enhance your subscription management." --- _Glassfy services will be ending in December 2024_. We worked with them to make the transition as easy as possible for you. This guide will help you migrate your subscribers to Adapty in less than a day. Most importantly, the migration will be 100% seamless for your customers; they will continue using the app without interruptions. :::info Moving from Glassfy? Get 6 months free of Pro+ plan When you migrate from Glassfy to Adapty, you can use all our features, including Paywall Builder, A/B tests, ML predictions, and Targeting for free for the first 6 months — no strings attached. Just use [this link](https://app.adapty.io/glassfy-migration-offer) to sign up. Try it for yourself and see why thousands of apps use Adapty to grow their revenue. ::: Here are the 3 easy steps to migrate your app from Glassfy to Adapty: 1. Learn the core differences (very few of them) and set up an Adapty account _(15 minutes)_; 2. Install Adapty SDK for your platform – [iOS](sdk-installation-ios), [Android](sdk-installation-android), [React Native](sdk-installation-reactnative), [Flutter](sdk-installation-flutter), [Unity](sdk-installation-unity) _(1 hour)_; 3. Test and release the new version of your app _(30 minutes)._ :::info Your subscribers will migrate automatically All users who have ever activated a subscription will instantly move to Adapty as soon as they open a new version of your app with the Adapty SDK. Subscription status validation and premium access will be restored automatically. ::: ### Learn the core differences and set up an Adapty account Adapty and Glassfy SDKs are similarly designed. Adapty allows you to show different paywalls to different audiences, but it's optional. Naming is slightly different: | Glassfy | Adapty | | :--------------------------------------------------------------- | :------------------------------- | | [SKU](https://docs.glassfy.io/docs/configure-products) | [Product](product) | | [Permission](https://docs.glassfy.io/docs/configure-permissions) | [Access level](access-level) | | [Offering](https://docs.glassfy.io/docs/configure-offerings) | [Paywall](paywalls) | #### Creating an Adapty account Create an account using a [special link ](https://app.adapty.io/glassfy-migration-offer). You can also [invite your colleagues](members-settings). #### Set up integration with the App Store and/or Google Play You've done it at least once already, so we'll just leave the link to the docs. You will have to provide a Bundle ID and subscription keys and set up server notifications so that Adapty can work with purchases. - [Configuring subscription key](app-store-connection-configuration#step-3-upload-in-app-purchase-key-file) and [enabling Apple server notifications](enable-app-store-server-notifications) for the App Store - [Configuring service account key file](google-play-store-connection-configuration#step-2-upload-the-account-key-file) and [enabling Google server notifications](enable-real-time-developer-notifications-rtdn) for the Google Play #### Create products To sell the product in Adapty SDK, you have to create it in the dashboard first. This process is very similar to how SKUs are created in Glassfy. Just give it a name, choose the access level (aka permission), and product IDs for the App Store / Google Play. You can read more about the products [here](product). #### Create paywalls Once you created the products, you should create the paywalls (aka offerings). A paywall can have one or more products. It can also have remote configuration, which allows you to customize the paywalls without new releases, localize the paywalls and onboarding and [much more](paywalls). You can even design and create paywalls without any coding with the [Adapty Paywall Builder](adapty-paywall-builder). #### Create placements Adapty has a concept of [placement](placements). It's a logical place inside your app where the user can make a purchase. In most cases, you have one or two placements: - Onboarding (because 80% of all purchases take place there) - General (you show it in settings or inside the app after the onboarding) With the placements, you can dynamically change which Paywall or A/B test should be displayed in the designated place of your application. You can even show different paywalls to different [audiences](audience) in your application. Well done, now you can integrate Adapty SDK into your app! ### Install Adapty SDK to replace Glassfy SDK Install Adapty SDK for your platform ([iOS](sdk-installation-ios), [Android](sdk-installation-android), [React Native](sdk-installation-reactnative), [Flutter](sdk-installation-flutter), [Unity](sdk-installation-unity). #### SDK activation **Glassfy** ```swift showLineNumbers func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { Glassfy.initialize(apiKey: "YOUR_API_KEY", watcherMode: false) [...] // optionally login your user Glassfy.login(user: "youruser") } ``` ```kotlin showLineNumbers class App : Application() { override fun onCreate() { super.onCreate() Glassfy.initialize(this, "YOUR_API_KEY", false, null) } } ``` ```java showLineNumbers public class App extends Application { @Override public void onCreate() { super.onCreate(); Glassfy.initialize(this, "YOUR_API_KEY", false, null); } } ``` ```javascript showLineNumbers try { await Glassfy.initialize('YOU_API_KEY',watcherMode: false); } catch (e) { // error [...] } ``` ```typescript showLineNumbers try { await Glassfy.initialize('YOU_API_KEY', false); } catch (e) { // initialization error } ``` **Adapty** ```swift showLineNumbers func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let configurationBuilder = AdaptyConfiguration .Builder(withAPIKey: "PUBLIC_SDK_KEY") .with(customerUserId: "YOUR_USER_ID") // optionally add your internal user id Adapty.activate(with: configurationBuilder.build()) { error in // handle the error } } ``` ```kotlin showLineNumbers override fun onCreate() { super.onCreate() Adapty.activate( applicationContext, AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withCustomerUserId(customerUserId) // optionally add your internal user id .build() ) } ``` ```java showLineNumbers @Override public void onCreate() { super.onCreate(); Adapty.activate( applicationContext, new AdaptyConfig.Builder("PUBLIC_SDK_KEY") .withCustomerUserId(customerUserId) // optionally add your internal user id .build() ); } ``` ```javascript showLineNumbers try { Adapty().activate(); } on AdaptyError catch (adaptyError) {} } catch (e) {} ``` ```typescript showLineNumbers adapty.activate('PUBLIC_SDK_KEY', { customerUserId: 'YOUR_USER_ID', // optionally add your internal user id }); ``` #### Fetch offerings (paywalls) **Glassfy** ```swift showLineNumbers Glassfy.offerings { (offerings, err) in if let offering = offerings?["premium"] { // display your offering's skus for sku in offering.skus { // sku.extravars // sku.product.localizedTitle // sku.product.localizedDescription // sku.product.price } } } ``` ```kotlin showLineNumbers Glassfy.offerings() { offers, err -> offers?.all ?.firstOrNull { it.offeringId == "premium" } ?.also { // display your offering's skus for (sku in it.skus) { // sku.extravars // sku.product.title // sku.product.description // sku.product.price } } } ``` ```java showLineNumbers Glassfy.offerings(new OfferingsCallback() { @Override public void onResult(@Nullable Offerings offers, @Nullable GlassfyError err) { Offering offering = null; if (offers != null) { for (Offering o : offers.getAll()) { if (o.getOfferingId().equals("premium")) { offering = o; } } } if (offering != null) { // display your offering's skus for (Sku sku : offering.getSkus()) { // sku.getExtravars(); // sku.getProduct().getTitle(); // sku.getProduct().getDescription(); // sku.getProduct().getPrice(); } } } }); ``` ```javascript showLineNumbers try { var offerings = await Glassfy.offerings(); var offering = offerings.all ?.singleWhere((offering) => offering.offeringId == 'premium'); offering?.skus?.forEach((sku) { // sku.product.description // sku.product.price }); } catch (e) { // initialization error [...] } ``` ```typescript showLineNumbers try { let offering = Glassfy.offerings().all.find((o) => o.offeringId === 'premium'); offering?.skus.forEach((sku) => { // sku.extravars // sku.product.description; // sku.product.price }); } catch (e) { [...] } ``` **Adapty** ```swift showLineNumbers Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en") { result in switch result { case let .success(paywall): // the requested paywall // call getPaywallProducts here case let .failure(error): // handle the error } } ``` ```kotlin showLineNumbers Adapty.getPaywall("YOUR_PLACEMENT_ID", locale = "en") { result -> when (result) { is AdaptyResult.Success -> { val paywall = result.value // the requested paywall // call getPaywallProducts here } 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 // call getPaywallProducts here } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` ```javascript showLineNumbers try { final paywall = await Adapty().getPaywall(id: "YOUR_PLACEMENT_ID", locale: "en"); // the requested paywall // call getPaywallProducts here } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ```typescript showLineNumbers try { const id = 'YOUR_PLACEMENT_ID'; const locale = 'en'; const paywall = await adapty.getPaywall(id, locale); // the requested paywall // call getPaywallProducts here } catch (error) { // handle the error } ``` ```swift showLineNumbers Adapty.getPaywallProducts(paywall: paywall) { result in switch result { case let .success(products): // the requested products array for product in products { // product.localizedTitle // product.localizedDescription // product.localizedPrice // product.localizedSubscriptionPeriod // product.price } case let .failure(error): // handle the error } } ``` ```kotlin showLineNumbers Adapty.getPaywallProducts(paywall) { result -> when (result) { is AdaptyResult.Success -> { val products = result.value // the requested products for (product in products) { // product.localizedTitle // product.localizedDescription // product.localizedPrice // product.localizedSubscriptionPeriod // product.price } } 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 for (AdaptyPaywallProduct product: products) { // product.localizedTitle // product.localizedDescription // product.localizedPrice // product.localizedSubscriptionPeriod // product.price } } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` ```javascript showLineNumbers try { final products = await Adapty().getPaywallProducts(paywall: paywall); // the requested products array for (var product in products!) { // product.localizedTitle // product.localizedDescription // product.price.amount // product.price.localizedString // product.subscriptionDetails.localizedSubscriptionPeriod } } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ```typescript showLineNumbers try { // ...paywall const products = await adapty.getPaywallProducts(paywall); // the requested products list } catch (error) { // handle the error } ``` In Adapty, you always request the paywall via [placement id](placements). If you want to see conversions, learn how [to log paywall views](present-remote-config-paywalls#track-paywall-view-events). #### Check permissions (access level) **Glassfy** ```swift showLineNumbers Glassfy.permissions { permissions, err in guard let permissions = permissions else { return } for p in permissions.all { switch (p.permissionId) { case "aPermission": if (p.isValid) { // unlock aFeature } break; default: print("Permission not handled"); break; } } } ``` ```kotlin showLineNumbers Glassfy.permissions { permission, err -> // update app status accordingly permission?.all?.forEach { when (it.permissionId) { "premium" -> if (it.isValid) { // unlock aFeature } else -> println("Permission not handled"); } } } ``` ```java showLineNumbers Glassfy.permissions(new PermissionsCallback() { @Override public void onResult(@Nullable Permissions permission, @Nullable GlassfyError error) { // update app status accondingly if (permission != null) { for (Permission p: permission.getAll()) { switch (p.getPermissionId()) { case "premium": if (p.isValid()) { // unlock aFeature } break; default: Log.d(TAG, "Permission not handled"); } } } } }); ``` ```javascript showLineNumbers try { var permission = await Glassfy.permissions(); permission.all?.forEach((p)=> { if (p.permissionId == "premium" && p.isValid==true) { // unlock aFeature } }); } catch (e) { // initialization error [...] } ``` ```typescript showLineNumbers try { const permissions = await Glassfy.permissions(); permissions.all.forEach((p)=>{ switch (p.permissionId) { case "premium": if (permission.isValid) { // unlock } break; default: break; } }); } catch (e) { // initialization 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 } } } ``` ```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 } }); ``` ```javascript showLineNumbers try { final profile = await Adapty().getProfile(); // check the access } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ```typescript showLineNumbers try { const profile = await adapty.getProfile(); } catch (error) { // handle the error } ``` **Adapty** #### Make a purchase **Glassfy** ```swift showLineNumbers Glassfy.purchase(sku: premiumSku) { (transaction, e) in // update app status accondingly if let p = transaction?.permissions["aPermission"] { if p.isValid { // unlock aFeature } else { // lock aFeature } } } ``` ```kotlin showLineNumbers Glassfy.purchase(activity, sku) { transaction, err -> // update app status accordingly transaction?.permissions ?.all ?.firstOrNull { it.permissionId == "aPermission" } ?.also { if (it.isValid) { // unlock aFeature } else { // lock aFeature } } } ``` ```java showLineNumbers Glassfy.purchase(activity, sku, new PurchaseCallback() { @Override public void onResult(@Nullable Transaction t, @Nullable GlassfyError err) { // update app status accordingly Permission permission = null; if (t != null) { for (Permission p : t.getPermissions().getAll()) { if (p.getPermissionId().equals("aPermission")) { permission = p; } } } if (permission != null) { if (permission.isValid()) { // unlock aFeature } else { // lock aFeature } } } }); ``` ```javascript showLineNumbers try { var transaction = await Glassfy.purchaseSku(sku); var p = transaction.permissions?.all?.singleWhere((permission) => permission.permissionId == 'premium'); if (p?.isValid==true) { // unlock aFeature } else { // lock aFeature } } catch (e) { // initialization error [...] } ``` ```typescript showLineNumbers try { const transaction = await Glassfy.purchaseSku(premiumSku ); const permission = transaction.permissions.all.find((p) => p.permissionId === "aPermission"); if (permission && permission.isValid) { // unlock aFeature } } catch (e) { // initialization error [...] } ``` **Adapty** ```swift showLineNumbers Adapty.makePurchase(product: product) { result in switch result { case let .success(info): if info.profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // successful purchase } case let .failure(error): // handle the error } } ``` ```kotlin showLineNumbers Adapty.makePurchase(activity, product) { result -> when (result) { is AdaptyResult.Success -> { val info = result.value //NOTE: info is null in case of cross-grade with DEFERRED proration mode val profile = info?.profile if (profile?.accessLevels?.get("YOUR_ACCESS_LEVEL")?.isActive == true) { // grant access to premium features } } is AdaptyResult.Error -> { val error = result.error // handle the error } } } ``` ```java showLineNumbers Adapty.makePurchase(activity, product, result -> { if (result instanceof AdaptyResult.Success) { AdaptyPurchasedInfo info = ((AdaptyResult.Success) result).getValue(); //NOTE: info is null in case of cross-grade with DEFERRED proration mode AdaptyProfile profile = info != null ? info.getProfile() : null; if (profile != null) { AdaptyProfile.AccessLevel premium = profile.getAccessLevels().get("YOUR_ACCESS_LEVEL"); if (premium != null && premium.isActive()) { // successful purchase } } } else if (result instanceof AdaptyResult.Error) { AdaptyError error = ((AdaptyResult.Error) result).getError(); // handle the error } }); ``` ```javascript showLineNumbers try { final profile = await Adapty().makePurchase(product: product); if (profile?.accessLevels['YOUR_ACCESS_LEVEL']?.isActive ?? false) { // successful purchase } } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ```typescript showLineNumbers try { const profile = await adapty.makePurchase(product); const isSubscribed = profile?.accessLevels['YOUR_ACCESS_LEVEL']?.isActive; if (isSubscribed) { // grant access to features in accordance with access level } } catch (error) { // handle the error } ``` ### Test and release a new version of your app If you're reading this, you've already: - [x] Configured integration with the App Store / Google Play - [x] Configured Adapty Dashboard - [x] Installed Adapty SDK - [x] Replaced Glassfy with Adapty functions - [ ] Made a sandbox purchase - [ ] Made a new app release If you checked the points above, just make a test purchase in the Sandbox and then release the app. :::info Go through [release checklist](release-checklist) Make the final check using our list to validate the existing integration or add additional features such as [attribution](attribution-integration) or [analytics](analytics-integration) integrations. ::: ### FAQ #### I successfully installed Adapty SDK and released a new app version with it. What will happen to my legacy subscribers who did not update to a version with Adapty SDK? Most users charge their phones overnight, it's when the App Store usually auto-updates all their apps, so it shouldn't be a problem. There may still be a small number of paid subscribers who did not upgrade, but they will still have access to the premium content. You don't need to worry about it and force them to update. #### Do I need to request historical data from Glassfy as quickly as possible, or will I lose it? You don't need to make it super fast; make a release with Adapty SDK first, and then give us your historical data. We will restore the history of your users' payments and fill in [profiles](profiles-crm) and [charts](charts). #### I use MMP (AppsFlyer, Adjust, etc.) and analytics (Mixpanel, Amplitude, etc.). How do I make sure that everything will work? You first need to pass us the IDs of such 3rd party services via our SDK that you want us to send data to. Read the guide for [attribution integration](attribution-integration) and for [analytics integration](analytics-integration). --- # File: migration-from-revenuecat.md --- --- title: "Migration from RevenueCat" description: "Migrate from RevenueCat to Adapty with our step-by-step guide." --- Your migration plan will have 5 logical steps and take an average of 2 hours. 90% of all migrations take less than one working day. 1. Learn the core differences; create and prepare an Adapty account _(5 minutes)_; 2. Install Adapty SDK for your platform ([iOS](sdk-installation-ios), [Android](sdk-installation-android), [React Native](sdk-installation-reactnative), [Flutter](sdk-installation-flutter), [Unity](sdk-installation-unity)) instead of RevenueCat SDK _(1 hour)_; 3. Set up [Apple App Store server notifications](enable-app-store-server-notifications) to Adapty and (optionally) [raw events forwarding](enable-app-store-server-notifications#raw-events-forwarding) _(5 minutes)_; 4. Test and release updates of your app _(30 minutes);_ 5. (Optional) Ask RevenueCat support for historical data in CSV format _(5 minutes);_ 6. (Optional) Import historical data via Adapty support _(30 minutes)_. :::info Your subscribers will migrate automatically All users who have ever activated a subscription will instantly move to Adapty as soon as they open a new version of your app with Adapty SDK. The subscription status validation and premium access will be restored automatically. ::: Before you push a new version of your app with Adapty SDK, make sure to check our [release сhecklist](release-checklist). ### Learn the core differences; create and prepare an Adapty account Adapty and RevenueCat SDKs are similarly designed. The biggest difference is the network usage and the speed: Adapty SDK is designed to provide you with information on demand as fast as possible when you ask for it. For example, when requesting a paywall, you get the [remote config](customize-paywall-with-remote-config) first to pre-build your onboarding or paywall and then request products in a dedicated request. Naming is slightly different: | RevenueCat | Adapty | | :---------- | :-------------- | | Package | Product | | Offering | Paywall | | Paywall | Paywall Builder | | Entitlement | Access level | Adapty has a concept of [placement](placements). It's a logical place inside your app where the user can make a purchase. In most cases, you have one or two placements: - Onboarding (because 80% of all purchases take place there); - General (you show it in settings or inside the app after the onboarding). ### Install Adapty SDK and replace RevenueCat SDK Install Adapty SDK for your platform ([iOS](sdk-installation-ios), [Android](sdk-installation-android), [React Native](sdk-installation-reactnative), [Flutter](sdk-installation-flutter), [Unity](sdk-installation-unity)) in your app. You need to replace a couple of SDK methods on the app side. Let's look at the most common functions and how to replace them with Adapty SDK. #### SDK activation Replace `Purchases.configure` with `Adapty.activate`. #### Getting paywalls (offerings) Replace `Purchases.shared.getOfferings` with [`Adapty.getPaywall`](fetch-paywalls-and-products#fetch-paywall-information). In Adapty, you always request the paywall via [placement id](placements). In practice, you only fetch no more than 1-2 paywalls, so we made this on purpose to speed up the SDK and reduce network usage. #### Getting a user (customer profile) Replace `Purchases.shared.getCustomerInfo` with `Adapty.getProfile`. #### Getting products In RevenueCat, you use the following structure:`Purchases.shared.getOfferings` and then `self.offering?.availablePackages`. In Adapty, you first request a paywall (read above) to get immediate access to Adapty's [remote config](customize-paywall-with-remote-config) and then call for products with [`Adapty.getPaywallProducts`](fetch-paywalls-and-products#fetch-products). #### Making a purchase Replace `Purchases.shared.purchase` with [`Adapty.makePurchase`](making-purchases#make-purchase). #### Checking access level (entitlement) Get a customer profile (read above first) and then replace `customerInfo?.entitlements["premium"]?.isActive == true` with [`profile.accessLevels["premium"]?.isActive == true`](subscription-status#retrieving-the-access-level-from-the-server). #### Restore purchase Replace `Purchases.shared.restorePurchases` with [`Adapty.restorePurchases`](restore-purchase). #### Check if the user is logged in Replace `Purchases.shared.isAnonymous` with `if profile.customerUserId == nil`. #### Log in user Replace `Purchases.shared.logIn` with [`Adapty.identify`](identifying-users#setting-customer-user-id-after-configuration). #### Log out user Replace `Purchases.shared.logOut` with [`Adapty.logout`](identifying-users#logging-out-and-logging-in). ### Switch App Store server-side notifications to Adapty Read how to do this [here](migrate-to-adapty-from-another-solutions#changing-apple-server-notifications). ### Test and release a new version of your app If you're reading this, you've already: - [x] Configured Adapty Dashboard - [x] Installed Adapty SDK - [x] Replaced SDK logic with Adapty functions - [x] Switched App Store server-side notifications to Adapty and optionally turn on raw events forwarding to RevenueCat - [ ] Made a sandbox purchase - [ ] Made a new app release If you checked the points above, just make a test purchase in the Sandbox and then release the app. :::info Go through [release checklist](release-checklist). Make the final check using our list to validate the existing integration or add additional features such as [attribution](attribution-integration) or [analytics](analytics-integration) integrations. ::: ### (Optional) Export your RevenueCat historical data in CSV format :::warning Don't rush the historical data import You should wait for at least a week after the release with the SDK before doing historical data import. During that time we will get all the info about purchase prices from the SDK, so the data you import will be more relevant. ::: Export your historical data from RevenueCat in CSV format by following the instructions in [RevenueCat’s official documentation](https://www.revenuecat.com/docs/integrations/scheduled-data-exports). ### (Optional) Ask RevenueCat support for Google Purchase Tokens If you need to import Google Play transactions, contact RevenueCat support for a CSV file containing Google Purchase Tokens via their [support page](https://app.revenuecat.com/settings/support). The Google Purchase Token is a unique identifier provided by Google Play for each transaction, essential for accurately tracking and verifying purchases in Adapty. This information is not included in the standard export file. The file contains the following three columns: - `user_id` - `google_purchase_token` - `google_product_id` ### Write us to import your historical data Contact us via the website messenger or email us at [support@adapty.io](mailto:support@adapty.io) with your CSV files. 1. Send the CSV file you exported from RevenueCat directly to our support team. 2. If importing Google Play transactions, include the CSV file with Google Purchase Tokens that you received from RevenueCat support. 3. Let us know which user ID should be used as the Customer User ID (Adapty’s primary user identifier): `rc_original_app_user_id` or `rc_last_seen_app_user_id_alias`. Our Support Team will import your transactions to Adapty. The following data will imported to Adapty for every transaction: | Parameter | Description | | ----------------------------- | ------------------------------------------------------------ | | user_id | Customer User ID, the main identifier of your user in Adapty and your system. | | apple_original_transaction_id | For subscription chains, this is the original transaction's purchase date, linked by `store_original_transaction_id`. | | google_product_id | The product ID in the Google Play Store. | | google_purchase_token | A unique identifier provided by Google Play for each transaction, required for validation. | | country | The country of the user. | | created_at | The date and time of the user creation. | | subscription_expiration_date | The date and time when the subscription expires. | | email | The end user's email. | | phone_number | The end user's phone number. | | idfa | The Identifier for Advertisers (IDFA), assigned by Apple to a user's device. | | idfv | The Identifier for Vendors (IDFV), a code assigned to all apps by one developer and shared across those apps on a device. | | advertising_id | A unique identifier provided by the Android OS that advertisers may use for tracking. | | attribution_channel | The marketing channel name. | | attribution_campaign | The marketing campaign name. | | attribution_ad_group | The attribution ad group. | | attribution_ad_set | The attribution ad set. | | attribution_creative | The attribution creative keyword. | In addition, integration identifiers for the following integrations will be imported: Amplitude, Mixpanel, AppsFlyer, Adjust, and FacebookAds. ### FAQ #### I successfully installed Adapty SDK and released a new app version with it. What will happen to my legacy subscribers who did not update to a version with Adapty SDK? Most users charge their phones overnight, it's when the App Store usually auto-updates all their apps, so it shouldn't be a problem. There may still be a small number of paid subscribers who did not upgrade, but they will still have access to the premium content. You don't need to worry about it and force them to update. #### Do I need to export my historical data from RevenueCat as quickly as possible, or will I lose it? You don't need to make it super fast; make a release with Adapty SDK first, and then give us your historical data. We will restore the history of your users' payments and fill in [profiles](profiles-crm) and [charts](charts). #### I use MMP (AppsFlyer, Adjust, etc.) and analytics (Mixpanel, Amplitude, etc.). How do I make sure that everything will work? You first need to pass us the IDs of such 3rd party services via our SDK that you want us to send data to. Read the guide for [attribution integration](attribution-integration) and for [analytics integration](analytics-integration). For historical data and legacy users, **make sure you pass us those IDs from the data you exported from RevenueCat.** --- # File: migration-guide-to-server-side-API-v2.md --- --- title: "Migration guide to server-side API v2" description: "" toc_max_heading_level: 5 --- 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](ss-set-transaction): Add transaction details with adding access.
  • [Grant Access Level](ss-grant-access-level): Add or extend access without transaction.
  • [Revoke Access Level](ss-revoke-access-level): 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](ss-grant-access-level):** Use this request to extend an access level without linking it to a transaction. 2. **[Revoke Access Level](ss-revoke-access-level):** to immediately revoke or shorten access. 3. **[Set Transaction](ss-set-transaction):** 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](ss-grant-access-level) 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](ss-grant-access-level) 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](ss-revoke-access-level) 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](ss-revoke-access-level) 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](ss-set-transaction) 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](ss-set-transaction) 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](ss-set-transaction) 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](ss-set-transaction) 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](ss-get-profile) 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](ss-update-profile) 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](ss-delete-profile) 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: migration-to-330.md --- --- title: "Migration guide to Adapty iOS SDK 3.3.x" description: "" --- Adapty SDK 3.3.0 is a major release that brought some improvements which however may require some migration steps from you. See our detailed migration guides per platforms: - [iOS](migration-to-ios330) - [Android](migration-to-android330) - [Flutter](migration-to-flutter330) - [Unity](migration-to-unity330) - [React Native](migration-to-react-native330) --- # File: migration-to-adapty-sdk-310.md --- --- title: "Migration guide to Adapty SDK 3.10.x" description: "" --- Adapty SDK 3.10.0 is a major release that brought some improvements which however may require some migration steps from you. See our detailed migration guides per platforms: - [Android](migration-to-android-310) - [Flutter](flutter-migration-guide-310) --- # File: migration-to-android-310.md --- --- 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.md --- --- 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-android-sdk-v3.md --- --- 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: migration-to-android330.md --- --- 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-flutter-sdk-34.md --- --- 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-flutter-sdk-v3.md --- --- 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_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: migration-to-flutter330.md --- --- 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 '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 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 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 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 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 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-ios-sdk-34.md --- --- 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-ios-sdk-v3.md --- --- 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: migration-to-ios330.md --- --- 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 - 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 - 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 - 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 - 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 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 - 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-new-paywall-builder.md --- --- title: "Migration to new Paywall Builder" description: "Migrate to the new Adapty Paywall Builder for an enhanced subscription flow." --- We’re thrilled to introduce our [**New Paywall Builder**](adapty-paywall-builder) ! This advanced no-code tool is designed to make creating custom paywalls more intuitive and powerful than ever before, allowing you to craft beautiful, engaging paywalls with ease. No technical or design expertise required! ## Key Features of the New Paywall Builder - **Expanded Template Selection**: Choose from a vast array of professionally designed templates to kickstart your paywall creation. These templates offer various styles and layouts to suit different needs and preferences. - **Enhanced Flexibility**: Enjoy greater flexibility with the ability to use design layers and new elements such as carousels, cards, product list, and footer. These enhancements give you the creative freedom to build any type of paywall you envision. - **Revamped Existing Elements**: Existing elements have been significantly improved, providing more options and capabilities to bring your paywall ideas to life. ## Parallel Paywall Builder Versions Adapty offers two versions of the Paywall Builder simultaneously: - **New Paywall Builder**: Located under the **Builder & Generator** tab of the paywall in the Adapty Dashboard, this version is the most recent and versatile. Paywalls created here require iOS, Android, and React Native SDKs version 3.0 or higher and Flutter and Unity SDKs version 3.3.0 or higher. - **Legacy Paywall Builder**: Found under the **Legacy Builder** tab, this outdated version should only be used to support older app versions with SDKs below v3.x.x. We recommend avoiding it for new paywalls as it will be deprecated soon. ## Migrating Paywalls to the New Builder Migrating a paywall from the legacy builder to the new builder will create a new version of your paywall in the **Builder & Generator** tab. This version can be edited using the new Paywall Builder and will display in apps with Adapty SDK v3.0 or later. The legacy version remains in the **Legacy Builder** tab and supports apps with SDK 2.x or earlier. You’ll maintain paywalls in both formats separately, with changes in one format not affecting the other. ## Steps to Migrate a Paywall To migrate a paywall to the new Paywall Builder: 1. Open the paywall you want to migrate. 2. Open the **Builder & Generator** tab. 3. Click **Migrate paywall**. 4. After the migration is done, review the result and make sure the paywall looks as it should. If not, correct it. 5. Click **Save**. 6. If there are some issues, they will be highlighted in red and you will see them at once. Fix them and save the paywall again. You can migrate your paywalls one at a time to review and fix them as needed. :::info Please note that paywalls created in the new Paywall Builder will only appear in app versions with Adapty SDK v3.0 or later. ::: ## We Value Your Feedback Your feedback is invaluable to us. If you encounter any issues or have suggestions for improvements, please reach out to us. We’re here to support you and enhance your experience with the new Paywall Builder. 📧 **Contact Us**: [Adapty Support](mailto:support@adapty.io) Enjoy building with the new Paywall Builder, and take your monetization strategy to the next level with our enhanced tools and features! --- # File: migration-to-react-native-sdk-34.md --- --- title: "Migrate Adapty React Native SDK to v. 3.4" description: "Migrate to Adapty React Native 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](react-native-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 - if (Platform.OS === 'android') { - try { - await adapty.restorePurchases(); - } catch (error) { - // handle the error - } - } const variationId = paywall.variationId; try { await adapty.reportTransaction(transactionId, variationId); } catch (error) { // handle the `AdaptyError` } ``` --- # File: migration-to-react-native-sdk-v3.md --- --- title: "Migrate Adapty React Native SDK to v. 3.0" description: "Migrate to Adapty React Native 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. ## Upgrade to version 3.0.1 1. Upgrade to version 3.0.1 as usual. 2. Replace the fallback paywall files: 1. [Download the latest version](fallback-paywalls#download-fallback-paywalls-as-a-file-in-the-adapty-dashboard) from the Adapty Dashboard. 2. Store them on the user's device and pass them to the `.setFallbackPaywalls` method as described [here](react-native-use-fallback-paywalls). --- # File: migration-to-react-native330.md --- --- title: "Migrate Adapty React Native SDK to v. 3.3" description: "Migrate to Adapty React Native SDK v3.3 for better performance and new monetization features." --- Adapty SDK 3.3.1 is a major release that brought some improvements that may require some migration steps from you. 1. Upgrade to Adapty SDK v3.3.x. 2. Update models. 3. Remove the `getProductsIntroductoryOfferEligibility` method. 4. Update making purchase. 5. Update Paywall Builder paywall presentation. 6. Revise the developer-defined timer implementation. 7. Update handling of Paywall Builder purchase events. 8. Update handling of Paywall Builder custom action events. 9. Modify the `onProductSelected` callback. 10. Remove third-party integration parameters from the `updateProfile` method. 11. Update integration configurations for Adjust, AirBridge, Amplitude, AppMetrica, Appsflyer, Branch, Facebook Ads, Firebase and Google Analytics, Mixpanel, OneSignal, and Pushwoosh. 12. Update Observer mode implementation. ## Upgrade Adapty React Native SDK to 3.3.x Before version 3.3.1, `react-native-adapty` SDK served as the core and mandatory SDK for Adapty to function properly in your app. The `@adapty/react-native-ui` SDK was optional and only needed if you were using the Adapty Paywall Builder. As of version 3.3.1, the `@adapty/react-native-ui` SDK is deprecated, and its functionality has been merged into the `react-native-adapty` SDK. To upgrade to version 3.3.1, follow these steps: 1. Update the `react-native-adapty` package to version 3.3.1. 2. Remove the `@adapty/react-native-ui` package from your project dependencies. 3. Sync your project dependencies to apply the changes. ## Changes in models ### New models 1. [AdaptySubscriptionOffer](https://react-native.adapty.io/interfaces/adaptysubscriptionoffer): ```typescript showLineNumbers export interface AdaptySubscriptionOffer { readonly identifier: AdaptySubscriptionOfferId; phases: AdaptyDiscountPhase[]; android?: { offerTags?: string[]; }; } ``` 2. [AdaptySubscriptionOfferId](https://react-native.adapty.io/types/adaptysubscriptionofferid): ```typescript showLineNumbers export type AdaptySubscriptionOfferId = | { id?: string; type: 'introductory'; } | { id: string; type: 'promotional' | 'win_back'; }; ``` ### Changed models 1. [AdaptyPaywallProduct](https://react-native.adapty.io/interfaces/adaptypaywallproduct): - Renamed the `subscriptionDetails` property to `subscription`.

```diff showLineNumbers - subscriptionDetails?: AdaptySubscriptionDetails; + subscription?: AdaptySubscriptionDetails; ``` 2. [AdaptySubscriptionDetails](https://react-native.adapty.io/interfaces/adaptysubscriptiondetails): - `promotionalOffer` is removed. Now the promotional offer is delivered within the `offer` property only if it's available. In this case `offer?.identifier?.type` will be `'promotional'`. - `introductoryOfferEligibility` is removed (offers are returned only if the user is eligible). - `offerId` is removed. Offer ID is now stored in `AdaptySubscriptionOffer.identifier`. - `offerTags` is moved to `AdaptySubscriptionOffer.android`.

```diff showLineNumbers - introductoryOffers?: AdaptyDiscountPhase[]; + offer?: AdaptySubscriptionOffer; ios?: { - promotionalOffer?: AdaptyDiscountPhase; subscriptionGroupIdentifier?: string; }; android?: { - offerId?: string; basePlanId: string; - introductoryOfferEligibility: OfferEligibility; - offerTags?: string[]; renewalType?: 'prepaid' | 'autorenewable'; }; } ``` 3. [AdaptyDiscountPhase](https://react-native.adapty.io/interfaces/adaptydiscountphase): - The `identifier` field is removed from the `AdaptyDiscountPhase` model. The offer identifier is now stored in `AdaptySubscriptionOffer.identifier`.

```diff showLineNumbers - ios?: { - readonly identifier?: string; - }; ``` ### Remove models 1. `AttributionSource`: - The string is now used in places where `AttributionSource` was previously used. 2. `OfferEligibility`: - This model has been removed as it is no longer needed. Now, an offer is returned only if the user is eligible. ## Remove `getProductsIntroductoryOfferEligibility` method Before Adapty SDK 3.3.1, product objects always included offers, even if the user wasn’t eligible. This required you to manually check eligibility before using the offer. Starting with version 3.3.1, the product object includes offers only if the user is eligible. This simplifies the process, as you can assume the user is eligible if an offer is present. ## Update making purchase In earlier versions, canceled and pending purchases were treated as errors and returned the codes `2: 'paymentCancelled'` and `25: 'pendingPurchase'`, respectively. Starting with version 3.3.1, canceled and pending purchases are now considered successful results and should be handled accordingly: ```typescript showLineNumbers try { const purchaseResult = await adapty.makePurchase(product); switch (purchaseResult.type) { case 'success': const isSubscribed = purchaseResult.profile?.accessLevels['YOUR_ACCESS_LEVEL']?.isActive; if (isSubscribed) { // Grant access to the paid features } break; case 'user_cancelled': // Handle the case where the user canceled the purchase break; case 'pending': // Handle deferred purchases (e.g., the user will pay offline with cash) break; } } catch (error) { // Handle the error } ``` ## Update Paywall Builder paywall presentation For updated examples, see the [Present new Paywall Builder paywalls in React Native](react-native-present-paywalls) documentation. ```diff showLineNumbers - import { createPaywallView } from '@adapty/react-native-ui'; + import { createPaywallView } from 'react-native-adapty/dist/ui'; const view = await createPaywallView(paywall); view.registerEventHandlers(); // handle close press, etc try { await view.present(); } catch (error) { // handle the error } ``` ## Update developer-defined timer implementation Rename the `timerInfo` parameter to `customTimers`: ```diff showLineNumbers - let timerInfo = { 'CUSTOM_TIMER_NY': new Date(2025, 0, 1) } + let customTimers = { 'CUSTOM_TIMER_NY': new Date(2025, 0, 1) } //and then you can pass it to createPaywallView as follows: - view = await createPaywallView(paywall, { timerInfo }) + view = await createPaywallView(paywall, { customTimers }) ``` ## Modify Paywall Builder purchase events Previously: - Canceled purchases triggered the `onPurchaseCancelled` callback. - Pending purchases returned error code `25: 'pendingPurchase'`. Now: - Both are handled by the `onPurchaseCompleted` callback. #### Steps to migrate: 1. Remove the `onPurchaseCancelled` callback. 2. Remove error code handling for `25: 'pendingPurchase'`. 3. Update the `onPurchaseCompleted` callback: ```typescript showLineNumbers const view = await createPaywallView(paywall); const unsubscribe = view.registerEventHandlers({ // ... other optional callbacks onPurchaseCompleted(purchaseResult, product) { switch (purchaseResult.type) { case 'success': const isSubscribed = purchaseResult.profile?.accessLevels['YOUR_ACCESS_LEVEL']?.isActive; if (isSubscribed) { // Grant access to the paid features } break; // highlight-start case 'user_cancelled': // Handle the case where the user canceled the purchase break; case 'pending': // Handle deferred purchases (e.g., the user will pay offline with cash) break; // highlight-end } // highlight-start return purchaseResult.type !== 'user_cancelled'; // highlight-end }, }); ``` ## Modify Paywall Builder custom action events Removed callbacks: - `onAction` - `onCustomEvent` Added callback: - New `onCustomAction(actionId)` callback. Use it for custom actions. ## Modify `onProductSelected` callback Previously, `onProductSelected` required the `product` object. Now requires `productId` as a string. ## Remove third-party integration parameters from `updateProfile` method Third-party integration identifiers are now set using the `setIntegrationIdentifier` method. The `updateProfile` method no longer accepts them. ## Update third-party integration SDK configuration To ensure integrations work properly with Adapty React Native SDK 3.3.1 and later, update your SDK configurations for the following integrations as described in the sections below. In addition, if you used the `AttributionSource` to get the attribution identifier, change your code to provide the required identifier as a string. ### 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 var adjustConfig = new AdjustConfig(appToken, environment); // Before submiting Adjust config... adjustConfig.setAttributionCallbackListener(attribution => { // Make sure Adapty SDK is activated at this point // You may want to lock this thread awaiting of `activate` adapty.updateAttribution(attribution, "adjust"); }); // ... Adjust.create(adjustConfig); + Adjust.getAdid((adid) => { + if (adid) + adapty.setIntegrationIdentifier("adjust_device_id", adid); + }); ``` ### 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 try { const deviceId = await Airbridge.state.deviceUUID(); - await adapty.updateProfile({ - airbridgeDeviceId: deviceId, - }); + await adapty.setIntegrationIdentifier("airbridge_device_id", deviceId); } catch (error) { // handle `AdaptyError` } ``` ### 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 try { - await adapty.updateProfile({ - amplitudeDeviceId: deviceId, - amplitudeUserId: userId, - }); + await adapty.setIntegrationIdentifier("amplitude_device_id", deviceId); + await adapty.setIntegrationIdentifier("amplitude_user_id", userId); } catch (error) { // handle `AdaptyError` } ``` ### 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 // ... const startupParamsCallback = async ( params?: StartupParams, reason?: StartupParamsReason ) => { const deviceId = params?.deviceId if (deviceId) { try { - await adapty.updateProfile({ - appmetricaProfileId: 'YOUR_ADAPTY_CUSTOMER_USER_ID', - appmetricaDeviceId: deviceId, - }); + await adapty.setIntegrationIdentifier("appmetrica_profile_id", 'YOUR_ADAPTY_CUSTOMER_USER_ID'); + await adapty.setIntegrationIdentifier("appmetrica_device_id", deviceId); } catch (error) { // handle `AdaptyError` } } } AppMetrica.requestStartupParams(startupParamsCallback, [DEVICE_ID_KEY]) ``` ### 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 appsFlyer.onInstallConversionData(installData => { try { - const networkUserId = appsFlyer.getAppsFlyerUID(); - adapty.updateAttribution(installData, AttributionSource.AppsFlyer, networkUserId); + const uid = appsFlyer.getAppsFlyerUID(); + adapty.setIntegrationIdentifier("appsflyer_id", uid); + adapty.updateAttribution(installData, "appsflyer"); } catch (error) { // handle the error } }); // ... appsFlyer.initSdk(/*...*/); ``` ### 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 branch.subscribe({ enComplete: ({ params, }) => { - adapty.updateAttribution(params, AttributionSource.Branch); + adapty.updateAttribution(params, "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 try { const anonymousId = await AppEventsLogger.getAnonymousID(); - await adapty.updateProfile({ - facebookAnonymousId: anonymousId, - }); + await adapty.setIntegrationIdentifier("facebook_anonymous_id", anonymousId); } catch (error) { // handle `AdaptyError` } ``` ### 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 try { const appInstanceId = await analytics().getAppInstanceId(); - await adapty.updateProfile({ - firebaseAppInstanceId: appInstanceId, - }); + await adapty.setIntegrationIdentifier("firebase_app_instance_id", appInstanceId); } catch (error) { // handle `AdaptyError` } ``` ### 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 // ... try { - await adapty.updateProfile({ - mixpanelUserId: mixpanelUserId, - }); + await adapty.setIntegrationIdentifier("mixpanel_user_id", mixpanelUserId); } catch (error) { // handle `AdaptyError` } ``` ### 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.User.pushSubscription.addEventListener('change', (subscription) => { const subscriptionId = subscription.current.id; if (subscriptionId) { - adapty.updateProfile({ - oneSignalSubscriptionId: subscriptionId, - }); + adapty.setIntegrationIdentifier("one_signal_subscription_id", subscriptionId); } }); ``` ```diff showLineNumbers OneSignal.addSubscriptionObserver(event => { const playerId = event.to.userId; - adapty.updateProfile({ - oneSignalPlayerId: playerId, - }); + adapty.setIntegrationIdentifier("one_signal_player_id", playerId); }); ``` ### 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 // ... try { - await adapty.updateProfile({ - pushwooshHWID: hwid, - }); + await adapty.setIntegrationIdentifier("pushwoosh_hwid", hwid); } catch (error) { // handle `AdaptyError` } ``` ## 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-react-native.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! ::: :::note Please pay attention that the order of the parameters for the `reportTransaction` method differs from the one for the `setVariationId` method. ::: ```diff showLineNumbers const variationId = paywall.variationId; try { - await adapty.setVariationId(variationId, transactionId); + await adapty.reportTransaction(transactionId, variationId); } catch (error) { // handle the `AdaptyError` } ``` --- # File: migration-to-unity-sdk-34.md --- --- title: "Migrate Adapty Unity SDK to v. 3.4" description: "Migrate to Adapty Unity 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](unity-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 - #if UNITY_ANDROID && !UNITY_EDITOR - Adapty.RestorePurchases((profile, error) => { - // handle the error - }); - #endif Adapty.ReportTransaction( "YOUR_TRANSACTION_ID", "PAYWALL_VARIATION_ID", // optional (error) => { // handle the error }); ``` --- # File: migration-to-unity-sdk-v3.md --- --- title: "Migrate Adapty Unity SDK to v. 3.0" description: "Migrate to Adapty Unity 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. ## Upgrade process The upgrade process for Unity involves the same steps as other platforms: 1. Upgrade to Adapty SDK v3.x 2. Migrate your existing paywalls to the new Paywall Builder For detailed Unity-specific migration instructions, please refer to the [Unity SDK installation guide](sdk-installation-unity) and follow the general migration steps outlined in the main migration guide. --- # File: migration-to-unity330.md --- --- title: "Migrate Adapty Unity SDK to v. 3.3" description: "Migrate to Adapty Unity 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. Upgrade to Adapty SDK v3.3.x. 2. Renamed multiple classes, properties, and methods in the Adapty and AdaptyUI modules of Adapty SDK. 3. From now on, the `SetLogLevel` method accepts a callback as an argument. 4. From now on, the `PresentCodeRedemptionSheet` method accepts a callback as an argument. 5. Change how the paywall view is created 6. Remove the `GetProductsIntroductoryOfferEligibility` method. 7. Save fallback paywalls to separate files (one per platform) in `Assets/StreamingAssets/` and pass the file names to the `SetFallbackPaywalls` method. 8. Update making purchase 9. Update handling of Paywall Builder events. 10. Update handling of Paywall Builder paywall errors. 11. Update integration configurations for Adjust, Amplitude, AppMetrica, Appsflyer, Branch, Firebase and Google Analytics, Mixpanel, OneSignal, Pushwoosh. 13. Update Observer mode implementation. 14. Update the Unity plugin initialization with an explicit `Activate` call. ## Upgrade Adapty Unity SDK to 3.3.x Up to this version, Adapty SDK was the core and mandatory SDK necessary for the proper functioning of Adapty within your app, and AdaptyUI SDK was an optional SDK that becomes necessary only if you use the Adapty Paywall Builder. Starting with version 3.3.0, AdaptyUI SDK is deprecated, and AdaptyUI is merged to Adapty SDK as a module. Because of these changes, you need to remove AdaptyUISDK and reinstall AdaptySDK. 1. Remove both **AdaptySDK** and **AdaptyUISDK** package dependencies from your project. 2. Delete the **AdaptySDK** and **AdaptyUISDK** folders. 3. Import the AdaptySDK package again as described in the [Adapty SDK installation & configuration for Unity](sdk-installation-unity) page. ## Renamings 1. Rename in Adapty module: | Old version | New version | | ------------------------- | ------------------------ | | Adapty.sdkVersion | Adapty.SDKVersion | | Adapty.LogLevel | AdaptyLogLevel | | Adapty.Paywall | AdaptyPaywall | | Adapty.PaywallFetchPolicy | AdaptyPaywallFetchPolicy | | PaywallProduct | AdaptyPaywallProduct | | Adapty.Profile | AdaptyProfile | | Adapty.ProfileParameters | AdaptyProfileParameters | | ProfileGender | AdaptyProfileGender | | Error | AdaptyError | 2. Rename in AdaptyUI module: | Old version | New version | | ------------------ | ------------------ | | CreatePaywallView | CreateView | | PresentPaywallView | PresentView | | DismissPaywallView | DismissView | | AdaptyUI.View | AdaptyUIView | | AdaptyUI.Action | AdaptyUIUserAction | ## Change the SetLogLevel method From now on, the `SetLogLevel` method accepts a callback as an argument. ```diff showLineNumbers - Adapty.SetLogLevel(Adapty.LogLevel.Verbose); + Adapty.SetLogLevel(Adapty.LogLevel.Verbose, null); // or you can pass the callback to handle the possible error ``` ## Change the PresentCodeRedemptionSheet method From now on, the `PresentCodeRedemptionSheet` method accepts a callback as an argument. ```diff showLineNumbers - Adapty.PresentCodeRedemptionSheet(); + Adapty.PresentCodeRedemptionSheet(null); // or you can pass the callback to handle the possible error ``` ## Change how the paywall view is created For the complete code example, check out [Fetch the view configuration of paywall designed using Paywall Builder](unity-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder). ```diff showLineNumbers + var parameters = new AdaptyUICreateViewParameters() + .SetPreloadProducts(true); - AdaptyUI.CreatePaywallView( + AdaptyUI.CreateView( paywall, - preloadProducts: true, + parameters, (view, error) => { // use the view }); ``` ## Remove the 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 method for providing fallback paywalls Up to this version, the fallback paywalls were passed as a serialized JSON. Starting from v 3.3.0, the mechanism is changed: 1. Save fallback paywalls to files in `/Assets/StreamingAssets/`, 1 file for Android and another for iOS. 2. Pass the file names to the `SetFallbackPaywalls` method. Your code will change this way: ```diff showLineNumbers using AdaptySDK; void SetFallBackPaywalls() { + #if UNITY_IOS + var assetId = "adapty_fallback_ios.json"; + #elif UNITY_ANDROID + var assetId = "adapty_fallback_android.json"; + #else + var assetId = ""; + #endif - Adapty.SetFallbackPaywalls("FALLBACK_PAYWALLS_JSON_STRING", (error) => { + Adapty.SetFallbackPaywalls(assetId, (error) => { // handle the error }); } ``` Check out the final code example in the [Use fallback paywalls in Unity](unity-use-fallback-paywalls) page. ## Update making purchase Previously canceled and pending purchases were considered errors and returned the `PaymentCancelled` and `PendingPurchase` codes, respectively. Now a new `AdaptyPurchaseResultType` class is used to process canceled, successful, and pending purchases. Update the code of purchasing in the following way: ```diff showLineNumbers using AdaptySDK; void MakePurchase(AdaptyPaywallProduct product) { - Adapty.MakePurchase(product, (profile, error) => { - // handle successfull purchase + Adapty.MakePurchase(product, (result, error) => { + switch (result.Type) { + case AdaptyPurchaseResultType.Pending: + // handle pending purchase + break; + case AdaptyPurchaseResultType.UserCancelled: + // handle purchase cancellation + break; + case AdaptyPurchaseResultType.Success: + var profile = result.Profile; + // handle successful purchase + break; + default: + break; } }); } ``` Check out the final code example in the [Make purchases in mobile app](unity-making-purchases) page. ## Update handling of Paywall Builder events Canceled and pending purchases are not considered to be errors any more, all these cases are processed with the `PaywallViewDidFinishPurchase` method. 1. Delete processing of the [Canceled purchase](unity-handling-events-legacy#canceled-purchase) event. 2. Update handling of the Successful purchase event in the following way: ```diff showLineNumbers - public void OnFinishPurchase( - AdaptyUI.View view, - Adapty.PaywallProduct product, - Adapty.Profile profile - ) { } + public void PaywallViewDidFinishPurchase( + AdaptyUIView view, + AdaptyPaywallProduct product, + AdaptyPurchaseResult purchasedResult + ) { } ``` 3. Update handling of actions: ```diff showLineNumbers - public void OnPerformAction( - AdaptyUI.View view, - AdaptyUI.Action action - ) { + public void PaywallViewDidPerformAction( + AdaptyUIView view, + AdaptyUIUserAction action + ) { switch (action.Type) { - case AdaptyUI.ActionType.Close: + case AdaptyUIUserActionType.Close: view.Dismiss(null); break; - case AdaptyUI.ActionType.OpenUrl: + case AdaptyUIUserActionType.OpenUrl: var urlString = action.Value; if (urlString != null { Application.OpenURL(urlString); } default: // handle other events break; } } ``` 4. Update handling of started purchase: ```diff showLineNumbers - public void OnSelectProduct( - AdaptyUI.View view, - Adapty.PaywallProduct product - ) { } + public void PaywallViewDidSelectProduct( + AdaptyUIView view, + string productId + ) { } ``` 5. Update handling of failed purchase: ```diff showLineNumbers - public void OnFailPurchase( - AdaptyUI.View view, - Adapty.PaywallProduct product, - Adapty.Error error - ) { } + public void PaywallViewDidFailPurchase( + AdaptyUIView view, + AdaptyPaywallProduct product, + AdaptyError error + ) { } ``` 6. Update handling of successful restore event: ```diff showLineNumbers - public void OnFailRestore( - AdaptyUI.View view, - Adapty.Error error - ) { } + public void PaywallViewDidFailRestore( + AdaptyUIView view, + AdaptyError error + ) { } ``` Check out the final code example in the [Handle paywall events](unity-handling-events) page. ## Update handling of Paywall Builder paywall errors The handling of errors is changed as well, please update your code according to the guidance below. 1. Update the handling of the product loading errors: ```diff showLineNumbers - public void OnFailLoadingProducts( - AdaptyUI.View view, - Adapty.Error error - ) { } + public void PaywallViewDidFailLoadingProducts( + AdaptyUIView view, + AdaptyError error + ) { } ``` 2. Update the handling of the rendering errors: ```diff showLineNumbers - public void OnFailRendering( - AdaptyUI.View view, - Adapty.Error error - ) { } + public void PaywallViewDidFailRendering( + AdaptyUIView view, + AdaptyError error + ) { } ``` ## Update third-party integration SDK configuration Starting with Adapty Unity 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 Unity 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 - using static AdaptySDK.Adapty; using AdaptySDK; Adjust.GetAdid((adid) => { - Adjust.GetAttribution((attribution) => { - Dictionary data = new Dictionary(); - - data["network"] = attribution.Network; - data["campaign"] = attribution.Campaign; - data["adgroup"] = attribution.Adgroup; - data["creative"] = attribution.Creative; - - String attributionString = JsonUtility.ToJson(data); - Adapty.UpdateAttribution(attributionString, AttributionSource.Adjust, adid, (error) => { - // handle the error - }); + if (adid != null) { + Adapty.SetIntegrationIdentifier( + "adjust_device_id", + adid, + (error) => { + // handle the error + }); } }); Adjust.GetAttribution((attribution) => { Dictionary data = new Dictionary(); data["network"] = attribution.Network; data["campaign"] = attribution.Campaign; data["adgroup"] = attribution.Adgroup; data["creative"] = attribution.Creative; String attributionString = JsonUtility.ToJson(data); - Adapty.UpdateAttribution(attributionString, AttributionSource.Adjust, adid, (error) => { + Adapty.UpdateAttribution(attributionString, "adjust", (error) => { // 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 using AdaptySDK; - var builder = new Adapty.ProfileParameters.Builder(); - builder.SetAmplitudeUserId("YOUR_AMPLITUDE_USER_ID"); - builder.SetAmplitudeDeviceId(amplitude.getDeviceId()); - Adapty.UpdateProfile(builder.Build(), (error) => { - // handle error - }); + Adapty.SetIntegrationIdentifier( + "amplitude_user_id", + "YOUR_AMPLITUDE_USER_ID", + (error) => { + // handle the error + }); + Adapty.SetIntegrationIdentifier( + "amplitude_device_id", + amplitude.getDeviceId(), + (error) => { + // 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 using AdaptySDK; - var deviceId = AppMetrica.GetDeviceId(); - if (deviceId != null { - var builder = new Adapty.ProfileParameters.Builder(); - builder.SetAppmetricaProfileId("YOUR_ADAPTY_CUSTOMER_USER_ID"); - builder.SetAppmetricaDeviceId(deviceId); - Adapty.UpdateProfile(builder.Build(), (error) => { - // handle error - }); - } + var deviceId = AppMetrica.GetDeviceId(); + if (deviceId != null { + Adapty.SetIntegrationIdentifier( + "appmetrica_device_id", + deviceId, + (error) => { + // handle the error + }); + + Adapty.SetIntegrationIdentifier( + "appmetrica_profile_id", + "YOUR_ADAPTY_CUSTOMER_USER_ID", + (error) => { + // 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 using AppsFlyerSDK; using AdaptySDK; // before SDK initialization AppsFlyer.getConversionData(this.name); // in your IAppsFlyerConversionData void onConversionDataSuccess(string conversionData) { // It's important to include the network user ID - string appsFlyerId = AppsFlyer.getAppsFlyerId(); - Adapty.UpdateAttribution(conversionData, AttributionSource.Appsflyer, appsFlyerId, (error) => { + string appsFlyerId = AppsFlyer.getAppsFlyerId(); + + Adapty.SetIntegrationIdentifier( + "appsflyer_id", + appsFlyerId, + (error) => { // handle the error }); + + Adapty.UpdateAttribution( + conversionData, + "appsflyer", + (error) => { + // 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 using AdaptySDK; - class YourBranchImplementation { - func initializeBranch() { - Branch.getInstance().initSession(launchOptions: launchOptions) { (data, error) in - if let data { - Adapty.updateAttribution(data, source: .branch) - } - } - } - } + Branch.initSession(delegate(Dictionary parameters, string error) { + string attributionString = JsonUtility.ToJson(parameters); + + Adapty.UpdateAttribution( + attributionString, + "branch", + (error) => { + // 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 // We suppose FirebaseAnalytics Unity Plugin is already installed using AdaptySDK; Firebase.Analytics .FirebaseAnalytics .GetAnalyticsInstanceIdAsync() .ContinueWithOnMainThread((task) => { if (!task.IsCompletedSuccessfully) { // handle error return; } var firebaseId = task.Result var builder = new Adapty.ProfileParameters.Builder(); - builder.SetFirebaseAppInstanceId(firebaseId); - - Adapty.UpdateProfile(builder.Build(), (error) => { - // handle error + Adapty.SetIntegrationIdentifier( + "firebase_app_instance_id", + firebaseId, + (error) => { + // 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 using AdaptySDK; - var builder = new Adapty.ProfileParameters.Builder(); - builder.SetMixpanelUserId(Mixpanel.DistinctId); - Adapty.UpdateProfile(builder.Build(), (error) => { - // handle error - }); + var distinctId = Mixpanel.DistinctId; + if (distinctId != null) { + Adapty.SetIntegrationIdentifier( + "mixpanel_user_id", + distinctId, + (error) => { + // 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 using AdaptySDK; - using OneSignalSDK; - var pushUserId = OneSignal.Default.PushSubscriptionState.userId; - var builder = new Adapty.ProfileParameters.Builder(); - builder.SetOneSignalPlayerId(pushUserId); - Adapty.UpdateProfile(builder.Build(), (error) => { - // handle error - }); + var distinctId = Mixpanel.DistinctId; + if (distinctId != null) { + Adapty.SetIntegrationIdentifier( + "mixpanel_user_id", + distinctId, + (error) => { + // 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 using AdaptySDK; - var builder = new Adapty.ProfileParameters.Builder(); - builder.SetPushwooshHWID(Pushwoosh.Instance.HWID); - Adapty.UpdateProfile(builder.Build(), (error) => { - // handle error - }); + Adapty.SetIntegrationIdentifier( + "pushwoosh_hwid", + Pushwoosh.Instance.HWID, + (error) => { + // 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-unity.md). ```diff showLineNumbers // every time when calling transaction.finish() - Adapty.SetVariationForTransaction("", "", (error) => { - if(error != null) { - // handle the error - return; - } - - // successful binding - }); + Adapty.ReportTransaction( + "YOUR_TRANSACTION_ID", + "PAYWALL_VARIATION_ID", // optional + (error) => { + // handle the error + }); ``` ## Update the Unity plugin initialization Starting from Adapty Unity SDK 3.3.0, calling the `Activate` method explicitly during the plugin initialization is required: ```csharp showLineNumbers Adapty.Activate(builder.Build(), (error) => { if (error != null) { // handle the error return; } }); ``` --- # File: mixpanel.md --- --- title: "Mixpanel" description: "Connect Mixpanel with Adapty for powerful subscription analytics." --- [Mixpanel](https://mixpanel.com/) is a powerful product analytics service. Its event-driven tracking solution empowers product teams to get valuable insights into optimal user acquisition, conversion, and retention strategies across different platforms. This integration enables you to bring all the Adapty events into Mixpanel. As a result, you'll gain a more comprehensive insight into your subscription business and customer actions. Adapty provides a complete set of data that lets you track [subscription events](events) from stores in one place. With Adapty, you can easily see how your subscribers are behaving, learn what they like, and use that information to communicate with them in a way that's targeted and effective. ## How to set up Mixpanel integration 1. Open the [Integrations -> Mixpanel](https://app.adapty.io/integrations/mixpanel) page in the Adapty Dashboard. 2. Enable the toggle and enter your **Mixpanel Token**. You can specify a token for all platforms or limit it to specific platforms if you only want to receive data from certain ones. ### Finding Your Mixpanel Token To get your **Mixpanel Token**: 1. Log in to your [Mixpanel Dashboard](https://mixpanel.com/settings/project/). 2. Open **Settings** and select **Organization Settings**. 3. From the left sidebar, go to **Projects** and select your project. ## How the integration works Adapty automatically maps relevant event properties—such as user ID and revenue—to [Mixpanel-native properties](https://help.mixpanel.com/hc/en-us/articles/115004708186-Profile-Properties). This ensures accurate tracking and reporting of subscription-related events. Additionally, Adapty accumulates revenue data per user and updates their [User Profile Properties](https://docs.mixpanel.com/docs/tracking/how-tos/user-profiles), including `subscription state` and `subscription product ID`. Once an event is received, Mixpanel updates the corresponding fields in real time. ## Events and tags Below the credentials, there are three groups of events you can send to Mixpanel from Adapty. Simply turn on the ones you need. Check the full list of the events offered by Adapty [here](events). We recommend using the default event names provided by Adapty. But you can change the event names based on your needs. ## SDK configuration Use `.setIntegrationIdentifier()` method to set `mixpanelUserId`. If not set, Adapty uses your user ID (`customerUserId`) or if it's null Adapty ID. Make sure that the user id you use to send data to Mixpanel from your app is the same one you send to Adapty. ```swift showLineNumbers do { try await Adapty.setIntegrationIdentifier( key: "mixpanel_user_id", value: Mixpanel.mainInstance().distinctId ) } catch { // handle the error } ``` ```swift showLineNumbers let builder = AdaptyProfileParameters.Builder() .with(mixpanelUserId: Mixpanel.mainInstance().distinctId) Adapty.updateProfile(params: builder.build()) ``` ```kotlin showLineNumbers Adapty.setIntegrationIdentifier("mixpanel_user_id", mixpanelAPI.distinctId) { error -> if (error != null) { // handle the error } } ``` ```javascript showLineNumbers final mixpanel = await Mixpanel.init("Your Token", trackAutomaticEvents: true); final distinctId = await mixpanel.getDistinctId(); try { await Adapty().setIntegrationIdentifier( key: "mixpanel_user_id", value: distinctId, ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` ```csharp showLineNumbers using AdaptySDK; var distinctId = Mixpanel.DistinctId; if (distinctId != null) { Adapty.SetIntegrationIdentifier( "mixpanel_user_id", distinctId, (error) => { // handle the error }); } ``` ```typescript showLineNumbers // ... try { await adapty.setIntegrationIdentifier("mixpanel_user_id", mixpanelUserId); } catch (error) { // handle `AdaptyError` } ``` --- # File: mrr.md --- --- title: "MRR" description: "Understand and optimize Monthly Recurring Revenue (MRR) in Adapty." --- The Monthly recurring revenue (MRR) chart displays the normalized revenue generated by your active paid subscriptions on a monthly basis. This chart enables you to understand your business's velocity and size, regardless of the fluctuations that may arise from varying subscription durations. ### Calculation Adapty calculates the predictable and recurring revenue components of your subscription business using the following formula: Where: Ps - subscription price Ns - number of active paid subscriptions for this subscription. Adapty considers any paid subscription that has not yet expired as an active subscription. Dsm - subscription duration in months (0.23 for weekly subscriptions) It is important to note that Adapty does not include non-recurring subscriptions, consumable, or one-time purchases in the calculation of Monthly Recurring Revenue (MRR). This is because these types of purchases do not represent predictable and recurring revenue. Basically, MRR shows revenue from all active subscriptions normalized to one month. For example, for a yearly subscription, instead of counting full revenue from the start, revenue is split into 12 equal parts which are evenly spread across 12 month period. E.g. if there are 2 active yearly subscriptions with price $240 and 10 monthly subscriptions with a price $30, MRR = (2 \* $240 / 12) + (10 \* $30 / 1) + (20 \* $10 / 0.23) = $1209.5 ### Available filters and grouping - ✅ Filter by: Attribution, country, paywall, store, product, and duration. - ✅ Group by: Renewal status, period, product, country, store, paywall, duration, attribution status, attribution channel, attribution campaign, attribution Ad group, attribution Ad set, and attribution creative. You can find more information about the available controls, filters, grouping options, tax and commission controls, and how to use them in [this documentation.](controls-filters-grouping-compare-proceeds) ### MRR chart usage MRR is a crucial metric for businesses that rely on recurring subscription revenue. It not only captures the size of your subscriber base but also standardizes different subscription durations to a common denominator (monthly recurring revenue). By doing so, MRR provides a real velocity metric for your business, making it easier to track your growth trajectory accurately. To leverage MRR effectively, segment your subscriber cohorts by their first purchase month and change the resolution to monthly. By doing this, you can create a stacked area chart that reveals how monthly subscriber cohorts have translated over time. This approach enables you to identify trends and patterns in your subscriber base, making it easier to adjust your business strategy and optimize your products and marketing efforts accordingly. ### Similar metrics In addition to MRR, Adapty also provides metrics for other revenue-related events, such as Revenue, ARR, ARPU, and ARPPU. To learn more about these revenue-related metrics, please refer to the following documentation guides: - [Revenue](revenue) - [ARR](arr) - [ARPU](arpu) - [ARPPU](arppu) --- # File: new-trials.md --- --- title: "New trials" description: "Manage new subscription trials and optimize trial-to-paid conversion rates." --- The new trial chart displays the number of activated trials during the selected time period. ### Calculation Adapty's calculation of the new trials refers to the number of trials initiated during a specific period. Adapty tracks the number of trials started within the selected period, regardless of their status (expired or active) at the end of the period. For example, if you select a monthly period and 50 users start a trial during that month, then the number of new trials initiated during that period would be 50. Similarly, if you choose a daily resolution, Adapty tracks the number of trials started each day, regardless of their status at the end of the day. ### Available filters and grouping - ✅ Filter by: Attribution, country, paywall, store, product, and duration. - ✅ Group by: Product, country, store, paywall, duration, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative. You can find more information about the available controls, filters, grouping options, and how to use them in[ this documentation](controls-filters-grouping-compare-proceeds). ### New trials chart usage The New trials chart is a powerful tool for tracking the effectiveness of your app's promotional campaigns and user acquisition efforts. For instance, if you run a targeted ad campaign on social media or search engines, you can use the New trials chart to monitor the number of new trials initiated during the campaign period. By analyzing this data, you can determine the effectiveness of the campaign and make data-driven decisions to optimize your promotional strategies in the future. ### Similar metrics In addition to New Trials, Adapty also provides metrics for other trial-related events, such as Active trials, Trial renewal cancelled, and Expired trials. To learn more about these trial-related metrics, please refer to the following documentation: - [Active trials](active-trials) - [Trial renewal cancelled](trials-renewal-cancelled) - [Expired trials](expired-churned-trials) --- # File: non-subscriptions.md --- --- title: "Non-subscriptions" description: "Learn how to manage non-subscription products in Adapty and track user purchases efficiently." --- The Non-subscriptions chart displays the number of in-app purchases such as consumables, non-consumables, and non-renewing subscriptions. The chart doesn't include renewable payments. The chart shows the total count of these types of in-app purchases and can help you track user behavior and engagement over time. ### Calculation Adapty's calculation logic for the Non-subscriptions chart involves counting the number of in-app purchases made by users that are classified as consumables, non-consumables, and non-renewing subscriptions. This chart excludes renewable payments such as auto-renewing subscriptions. - Consumables are items that users can purchase multiple times, such as fish food in a fishing app, or extra in-game currency. - Non-consumables are items that users can purchase once and use forever, such as a race track in a game app, or ad-free versions of an app. - Non-renewing subscriptions are subscriptions that expire after a set period of time and do not renew automatically, such as a one-year subscription to a catalog of archived articles. The content of this in-app purchase can be static, but the subscription will not renew automatically once it expires. ### Available filters and grouping - ✅ Filter by: Attribution, country, paywall, and store. - ✅ Group by: Product, country, store, paywall, attribution status, attribution channel, attribution campaign, attribution ad group, attribution ad set, and attribution creative. You can find more information about the available controls, filters, grouping options, and how to use them in [this documentation.](controls-filters-grouping-compare-proceeds) ### Non-subscriptions chart usage The Non-subscriptions chart is an important tool for app developers to gain insights into the types of in-app purchases made by users, including consumables, non-consumables, and non-renewing subscriptions. By tracking this metric over time, you can better understand user behavior and engagement with their app. Using the Non-subscriptions chart with filters and grouping, you can dive deeper into their users' purchase patterns and preferences, helping them to optimize pricing strategies and improve overall user satisfaction. The Non-subscriptions chart is an essential tool for app to make data-driven decisions that ultimately lead to a better user experience and increased revenue. ### Similar metrics In addition to non-subscriptions, Adapty also provides metrics for other subscription-related events, such as active subscriptions, new subscriptions, subscriptions renewal canceled, and expired subscriptions. To learn more about these subscriptions-related metrics, please refer to the following documentation guides: - [Active subscriptions](active-subscriptions) - [New subscriptions](reactivated-subscriptions) - [Churned (expired) subscriptions](churned-expired-subscriptions) - [Cancelled subscriptions](cancelled-subscriptions) --- # File: observer-vs-full-mode.md --- --- title: "Observer mode" description: "Compare Observer Mode and Full Mode in Adapty for subscriptions." --- Adapty is a powerful and flexible in-app purchase platform designed to boost your revenue and subscriber base. With features like customizable paywalls tailored to specific user segments, A/B testing for pricing, duration, trial periods, and visual elements, as well as comprehensive analytical tools for app monetization and third-party integrations, Adapty empowers your growth strategy. However, if you already have your own purchase infrastructure and aren't prepared to switch to Adapty's system, you can explore the Adapty Observer mode. This limited mode omits the use of Adapty paywalls, targeting them to user audiences, managing the subscriptions, including handling renewals and billing retries, and focusing solely on analytics. Despite its limitations, Observer mode still offers robust analytics capabilities, including integration with attribution systems, advanced analytics, messaging, and CRM profiles. Both modes are offered at the same price and require your mobile app to be updated, so the choice essentially comes to either transitioning to Adapty's infrastructure for full functionality or retaining your current infrastructure while only gaining third-party integrations and analytical capabilities. | Functionality | Observer mode | Full mode | |-------------|-------------|---------| | **Comprehensive Analytics** | ✅ | ✅ | | **Third-Party Integrations** | ✅ | ✅ | | **Responding to purchase events to give/restrict paid access to your users** | ❌ | ✅ | | **Purchases Infrastructure Maintainer** | You | Adapty | | **A/B Testing** |

:warning:

Feasible, but requires a significant amount of additional coding and configuration, more than in Full Mode.

| ✅ | | **Implementation Time** |

For analytics and integrations: Less than an hour

With A/B tests: Up to a week with thorough testing

| Several hours | ## How Observer Mode works In Observer mode, you report new transactions from Apple/Google to the Adapty SDK, and Adapty SDK forwards them to the Adapty backend. You are tasked with managing access to the paid content in their app, completing transactions, handling renewals, addressing billing issues, and so on. ## How to set up Observer mode 1. Set up initial integration of Adapty [with Google Play](initial-android) and [with App Store](initial_ios). 2. Turn it on when configuring the Adapty SDK by setting the `observerMode` parameter to `true`. Follow the setup instructions for [iOS](sdk-installation-ios#configure-adapty-sdk), [Android](sdk-installation-android#configure-adapty-sdk), [Flutter](sdk-installation-flutter#configure-adapty-sdk), [React Native](sdk-installation-reactnative#configure-adapty-sdks), and [Unity](sdk-installation-unity#configure-adapty-sdk). 3. [Report transactions](report-transactions-observer-mode) from your existing purchase infrastructure to Adapty for iOS and iOS-based cross-platform frameworks. 4. (optional) If you want to use 3d-party integrations, set them up as described in the [Configure 3d-party integration](configuration) topic. :::warning When operating in Observer mode, the Adapty SDK does not finalize transactions, so ensure you handle this aspect yourself. ::: ## How to use paywalls and A/B tests in Observer mode In Observer mode, Adapty SDK cannot determine the source of purchases as you make them in your own infrastructure. 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 when you report a transaction. Additionally, paywalls designed with Paywall Builder should be displayed in a special way when using the Observer mode: - Display paywalls in Observer mode for [iOS](implement-observer-mode) or [Android](android-present-paywall-builder-paywalls-in-observer-mode). - [Associate paywalls to purchase transactions](report-transactions-observer-mode) when reporting transactions in Observer mode. --- # File: offers.md --- --- title: "Offers" description: "Set up and manage subscription offers in Adapty to drive conversions." --- Offers in the App Store and Google Play are special deals or discounts provided by these platforms for in-app purchases. There are the following types of offers: - **Introductory offer** An introductory offer is a special welcome for users who are exploring a subscription-based app for the first time. It's a promotion that you can set up to provide new subscribers with a discounted price, a free trial, or other enticing deals for a certain period. - **Promotional offer** A promotional offer is a friendly invitation for users who are already familiar with a subscription-based app. It's a special deal or discount that you can create to engage existing or past subscribers. With promotional offers, you can provide discounted prices, free trials, or other enticing deals to encourage users to renew or re-subscribe. - **Win-back offer** A win-back offer is a strategic deal aimed at re-engaging users who have previously canceled or let their subscriptions lapse. This type of offer helps you reconnect with churned subscribers by providing exclusive discounts or promotions, enticing them to return and resubscribe. Winback offers are a great way to reduce churn and regain lost users. :::note Introductory offers on iOS are applied automatically if the user is eligible. Do not create them in Adapty. ::: These offers help attract and keep users engaged, making the app experience more rewarding. By using these special incentives, you can boost user interest and loyalty, contributing to the overall success of their apps. :::note Checklist for Adapty to successfully process offers from the App Store and Play Store: 1. [Create offers in the App Store Connect](app-store-offers) or [create offers in the Google Play Console](google-play-offers) 2. (for iOS apps only) [Upload a special In-App Purchase Key from App Store Connect to Adapty](app-store-connection-configuration#step-4-for-trials-and-special-offers--set-up-promotional-offers). 3. [Create offers in Adapty](create-offer) 4. [Add these offers to a paywall in Adapty](add-offer-to-paywall) ::: --- # File: onboarding-actions.md --- --- title: "Onboarding actions" description: "Configure actions—navigate, open paywalls, fire events, and close flows—in Adapty’s no-code onboarding builder." --- Actions are the interactive behaviors you assign to onboarding elements, allowing them to respond to user input or handle events. By setting a trigger (like a button press or loader completion) and selecting an action type, you control how users move through and interact with your onboarding flow. :::tip Learn more about branching onboarding flows in the detailed article. ::: ## Add actions The configuration process depends on the element you attach the action to. You can add actions to the following elements: - **Buttons**: Configure actions in the [**On Press** dropdown of the **Element** tab](onboarding-buttons.md#add-buttons). - **Quizzes**: Configure actions in the [**Behaviour** section of the **Element** tab](onboarding-quizzes.md#step-2-configure-navigation). - **Loaders**: Configure actions in the **Complete action** section of the **Element** tab. For example, here's where to find it for quizzes: ## Action types When configuring actions, choose one of the following types: #### Navigate Moves the user to another onboarding screen, letting you control flow based on user actions or selections. Ideal for chaining multiple actions for multi-step logic with quizzes. #### Show/Hide element Toggles the visibility of a specified element for conditional content within a screen. Use this to display extra content only when users need it. #### Open paywall Launches your app’s paywall to present purchases or subscriptions. Learn how to handle opening paywall on [iOS](ios-handling-onboarding-events.md#opening-a-paywall), [Android](android-handle-onboarding-events.md#opening-a-paywall), [Flutter](flutter-handling-onboarding-events.md#opening-a-paywall), and [React Native](react-native-handling-onboarding-events.md#opening-a-paywall). #### Scroll to Programmatically scrolls the view to a target element on the current screen. Helpful for long-form screens when a “See details” button is pressed. #### Custom Allows you to define and execute your own logic based on the [action ID](#action-id). Use this action to trigger behaviors not covered by the standard action types. Learn how to handle custom action on [iOS](ios-handling-onboarding-events.md#custom-actions), [Android](android-handle-onboarding-events.md#custom-actions), [Flutter](flutter-handling-onboarding-events.md#handle-custom-actions), and [React Native](react-native-handling-onboarding-events.md#handle-custom-actions). #### Close onboarding Ends the onboarding flow and closes the interface. Use when users finish setup to immediately drop back into the main app. Learn how to handle onboarding closure on [iOS](ios-handling-onboarding-events.md#closing-onboarding), [Android](android-handle-onboarding-events.md#closing-onboarding), [Flutter](flutter-handling-onboarding-events.md#closing-onboarding), and [React Native](react-native-handling-onboarding-events.md#closing-onboarding). ## Action triggers Actions fire depending on the element they’re attached to: - **Button**: Runs when a user clicks a button or when a timer completes. - **Quiz**: Executes when an option is selected. - **Loader**: Triggers after a Loader or Processing finishes. ## Action ID When setting up custom actions for buttons, you may want to handle different buttons the same way using action IDs: 1. When [adding a button](onboarding-buttons.md#add-buttons), assign it an ID in the **On Press** section of the **Element** tab. 2. [Use the assigned action ID in your source code](ios-handling-onboarding-events.md#custom-actions). :::note Action ID is not the same as the [element ID](onboarding-variables.md) used for inserting dynamic data with variables. Be sure not to mix them up. ::: --- # File: onboarding-buttons.md --- --- title: "Onboarding buttons" description: "Add buttons to navigate users between screens, close the onboarding or move to the paywall." --- Learn how to add and configure standard, animated, glossy, and countdown buttons in Adapty's no-code onboarding builder. Guide users, drive conversions, and close your flow—all without writing a single line of code. ## Add buttons Use a Pulse Button to draw attention and boost click-through rates. Or add a Countdown Button on trial expiration slides to create urgency and increase upgrades. To add a button: 1. Click **Add** at the top left. 2. Select **Buttons** and choose one: - **Button** - **Pulse Button** - **Glossy Button** - **Pulse Glossy Button** - **Countdown Button** 3. Choose the [button action](onboarding-actions.md) from the **On Press** dropdown on the right: - **Navigate**: Moves the user to a specified onboarding screen. - **Show/Hide element**: Shows or hides a target element. - **Open paywall**: Opens the paywall screen for purchases. Learn how to handle opening paywall on [iOS](ios-handling-onboarding-events.md#opening-a-paywall), [Android](android-handle-onboarding-events.md#opening-a-paywall), [Flutter](flutter-handling-onboarding-events.md#opening-a-paywall), and [React Native](react-native-handling-onboarding-events.md#opening-a-paywall). - **Scroll to**: Scrolls the page to a specific element. - **Custom**: Runs your custom event logic. For example, ut can be used for opening a login window or requesting the app permissions. Learn how to handle custom action on [iOS](ios-handling-onboarding-events.md#custom-actions), [Android](android-handle-onboarding-events.md#custom-actions), [Flutter](flutter-handling-onboarding-events.md#handle-custom-actions), and [React Native](react-native-handling-onboarding-events.md#handle-custom-actions). - **Close onboarding**: Closes the onboarding flow. Learn how to handle onboarding closure on [iOS](ios-handling-onboarding-events.md#closing-onboarding), [Android](android-handle-onboarding-events.md#closing-onboarding), [Flutter](flutter-handling-onboarding-events.md#closing-onboarding), and [React Native](react-native-handling-onboarding-events.md#closing-onboarding). To edit button text, click the button preview and make your changes in WYSIWYG mode. :::tip [Nest a popup](onboarding-layout.md#containers) with a Pulse Glossy Button to upsell premium features mid‑flow. ::: ## Button customization Beyond the basic [element layout](onboarding-layout.md#element-layout), you can customize button appearance: 1. Select the button element on the left. 2. Go to **Styles** in the right menu. 3. Based on the button type, you can adjust these options: - **All buttons**: Width, padding, background, roundness, border, border color, shadows, next arrow and arrow size, right offset, text or countdown color, font, and line height. - **Pulse Button**: Animation duration and easing, shadow color and size, button grow. - **Glossy Button**: Glossy line color, width, angle, and animation duration. - **Pulse Glossy Button**: Animation duration and easing, shadow color and size, button grow, glossy line color, width, angle, and animation duration. --- # File: onboarding-element-visibility.md --- --- title: "Onboarding element visibility" description: "Configure static and dynamic navigation in Adapty’s no-code onboarding builder to guide users through flows." --- You can also add conditional visibility to specific elements. Conditional elements are only visible to users who gave specific quiz answers. For example, users who answered "What's your experience level" with "Beginner" will see extra details on the next step. To make an element conditional: 1. Go to the **Element** tab on the right. 2. Select **Conditional** in the **Visible** section. 3. Set up the condition by choosing: - quiz ID - operator - quiz answer 4. (Optional) Click **Advanced condition** to add multiple conditions. For example, if you selected 'goal' as quiz ID, 'Has' as operator, and 'Education' as quiz answer, the element will be visible only to users whose answers for that quiz include the 'Education' option. --- # File: onboarding-html.md --- --- title: "Custom HTML" description: "Embed small, lightweight HTML snippets in Adapty's no-code onboarding builder to create interactive widgets and third-party embeds." --- Custom HTML lets you create unique interactions, embed third-party widgets, or quickly test experimental elements without app updates. :::note Custom HTML elements are not preloaded or cached, so we recommend using raw HTML only for small, lightweight elements. ::: To insert custom HTML code: 1. Click **Add** at the top left. 2. Go to **Media & Display** and choose **Raw HTML**. 3. Insert or edit your HTML code on the right. --- # File: onboarding-layout.md --- --- title: "Onboarding layout" description: "Adapty onboarding builder: containers for layout, tweak element spacing and style." --- The no-code mobile app onboarding builder offers two layout layers: - Screen layout: global padding and grid via containers. - Element layout: per-element spacing, position, borders, and shadows. :::tip To reorder screens or elements, simply drag and drop them in the left pane. ::: ## Screen layout You can adjust a screen in two ways: - [Using screen style settings](#screen-style-settings) - [Using containers](#containers) ### Screen style settings To reduce or increase the distance between elements and the screen edge: 1. Select the screen on the left. 2. Go to the **Styles** tab on the right. 3. Set the top, bottom, and horizontal padding in the **Padding** section. ### Containers You may want to add side-by-side text and images, swipeable galleries, or modal pop-ups. Containers make this easy by letting you create columns, rows, carousels, and centered overlays. To add a container: 1. Click **Add** at the top left. 2. Go to **Containers** and choose one: - **Columns**: Split the screen into vertical sections for side‑by‑side content (e.g., two-column text or image-plus-copy layouts). - **Rows**: Line up items in a single horizontal band with even spacing. - **Carousel**: Let users swipe through a series of cards. - **Popup**: Show content in a centered overlay above the page. 3. Create the elements you want to add, then drag&drop them into the container from the left menu. ## Element layout To adjust each element individually: 1. Select the element on the left. 2. Go to **Styles** on the right menu. 3. In the **Container** section set: - **Offset**: Shifts the element horizontally or vertically. - **Position**: Sets the element's anchor point: - **In content**: Normal document flow - **Attached**: Fixed position - stays visible in viewport (e.g., sticky button at bottom) - **Attached on scroll**: Becomes fixed after scrolling into view (sticky behavior) - **Padding**: Defines the inner space between the element's content and its border. - **Background**: Applies a solid color behind the element. Ensure your element background matches the [screen background](#screen-background-customization) (e.g., use grey or black for onboardings with mostly dark screens). - **Roundness**: Determines the radius of the element's corners. - **Border**: Adds a stroke around the element and specifies its thickness. - **Border Color**: Specifies the color of the element's border. - **Add shadows**: Adds a single drop shadow with configurable offset, blur/spread, and color. :::note In addition to these basic element layout settings, you can further customize the appearance of specific elements like [media](onboarding-media.md#media-customization), [text](onboarding-text.md#text--list-customization), [buttons](onboarding-buttons.md#button-customization), [quizzes](onboarding-quizzes.md#quiz-customization) and others using the **Styles** tab for the element. ::: ## Screen background customization The background affects not only your onboarding design but also the loading screen until the onboarding is fully loaded. You can fill your onboarding background with a color or upload an image/video: 1. Select the screen on the left. 2. Go to the **Styles** tab on the right. 3. In the **Background** section, select a background color or click the upload area to upload an image/video. For media uploads, follow the [supported formats and size](onboarding-media.md#supported-formats-and-size) requirements. :::tip For smooth screen transitions, choose a background color that matches your overall onboarding design (e.g., use grey or black for onboardings with mostly dark screens) or customize the [splash screen](ios-present-onboardings#add-smooth-transitions-between-the-splash-screen-and-onboarding). ::: --- # File: onboarding-media.md --- --- title: "Onboarding media" description: "Create engaging Adapty onboarding flows with images, videos, animated charts, and custom backgrounds." --- Rich media elements help you create engaging onboarding experiences that demonstrate your app's value and guide users toward conversion. Use images and videos to showcase features, animated charts to visualize benefits, and strategic backgrounds to reinforce your brand. ## Images and videos Images and videos are perfect for feature previews and app tours. Showing users what they'll unlock is more effective than describing it. To upload media: 1. Click **Add** at the top left. 2. Go to **Media & Display** and choose **Image/Video**. 3. Click the upload area on the right and select your image or video to upload. ### Supported formats and size | Specification | Details | |-------------------|----------------------------------| | Extensions | PNG, JPG, JPEG, WEBP, MP4, WEBM | | Maximum file size | 15 MB | If you want to add an unsupported animated element (like Lottie), you can convert it to a video (for example, with [this tool](https://www.lottielab.com/lottie-to-video)) and embed it as a video. ## Animated charts Charts are animations that visualize results and personalize the user experience in your onboardings. To add a chart: 1. Click **Add** at the top left. 2. Go to **Media & Display** and choose **Chart**. 3. Customize your chart on the right: - **Type**: Choose a curve type. Note that the curve type is not directly connected to the values. - **Left** and **Right badges**: Name the initial and final points of the chart. - **X Labels** and **Date Range**: By default, the X-axis displays dates. You can customize the date range or specify custom values. - **Animation Duration**: Set the animation duration to fit your design. :::tip Use [variables](onboarding-variables.md) for dynamic data visualization in charts. ::: ## Media customization In addition to the basic [element layout](onboarding-layout.md#element-layout), you can further customize the appearance of images, videos, and charts: 1. Select the element on the left. 2. Go to **Styles** on the right menu. 3. Based on the element type, you can adjust the following options: - **Image/video**: Width, height, roundness, opacity, alignment. - **Chart**: Line color and width, badge padding, roundness, font and color, X-axis font and color. ## Delete media You can delete the entire media element or just the file to upload a new one: - **Delete media element**: Right-click the media element on the left and select **Delete**. - **Delete media file**: Click the media preview on the right. The upload area for your new file will appear. --- # File: onboarding-metrics.md --- --- title: "Onboarding metrics" description: "Track and analyze onboarding performance metrics to improve subscription revenue." --- Adapty collects a series of metrics to help you better measure the performance of the onboardings. All metrics are updated in real-time, except for the views, which are updated once every several minutes. This document outlines the metrics available, their definitions, and how they are calculated. Onboarding metrics are available on the onboarding list, providing you with an overview of the performance of all your onboardings. This consolidated view presents aggregated metrics for each onboarding, allowing you to assess their effectiveness and identify areas for improvement. For a more granular analysis of each onboarding, you can navigate to the onboarding detail metrics. In this section, you will find comprehensive metrics specific to the selected onboarding, offering deeper insights into its performance. ## Metrics controls The system displays the metrics based on the selected time period and organizes them according to the left-side column parameter with three indentation levels. For Live onboardings, the metrics cover the period from the onboarding's start date until the current date. For inactive onboardings, the metrics encompass the entire period from the start date to the end of the selected time period. Draft and archived onboardings are included in the metrics table, but if no data is available, they will be listed without any displayed metrics. ### View options for metrics data The onboarding page offers two viewing options for metrics data: - Placement-based view: Metrics are grouped by placements associated with the onboarding. This allows users to analyze metrics by different placements. - Audience-based view: Metrics are grouped by the target audience of the onboarding. Users can assess metrics specific to different audience segments. The dropdown at the top of the onboarding page allows you to select the preferred view. ### Filter metrics by install date The **Filter metrics by install date** checkbox lets you analyze data based on when users installed your app. This helps you measure how well you're acquiring new users during specific time periods. It's a handy option when you want to customize your analysis. ### Time ranges You can analyze metrics data using a time range, allowing you to focus on specific durations such as days, weeks, months, or custom date ranges. ### Filters and groups Adapty offers powerful tools for filtering and customizing metrics analysis to suit your needs. Adapty's metrics page gives you access to various time ranges, grouping options, and filtering possibilities. - Filter by: Attribution, Country, Onboarding audience, Onboarding A/B tests, Onboarding placement, Paywall, Store, State. - Group by: Product or Store. ### Single metric chart The chart section shows your data in a simple bar graph. The chart helps you quickly see: - The exact numbers for each metric. - Period-specific data. A total sum appears next to the chart, giving you the complete picture at a glance. Click the arrow icon to expand the chart. ### Total metrics summary Next to the single metrics chart, there is a total metrics summary section. This section shows the cumulative values for the selected metrics at a specific point in time. You can change the displayed metric using a dropdown menu. ## Metrics definitions ### Views & unique views Views count the number of times users see your onboarding. If someone visits twice, that's two **views** but one **unique view**. This metric helps you understand how often your onboarding has been shown. ### Completions & unique completions Completions count the number of times users complete your onboarding, meaning that they go from the first to the last screen. If someone completes it twice, that's two **completions** but one **unique completion**. ### Unique completions rate The unique completion number divided by the unique view number. This metric helps you understand how people engage with onboarding and make changes if you notice that people ignore it. ### Revenue **Revenue** shows your total earnings in USD from purchases and renewals. This is the amount before any deductions. ### Proceeds [**Proceeds**](analytics-cohorts#revenue-vs-proceeds) shows what you receive after App Store/Play Store takes their commission, but before taxes. ### Net proceeds Your final earnings after both store commissions and taxes are deducted. ### ARPPU ARPPU is the average revenue per paying user. It’s calculated as total revenue divided by the number of unique paying users. $15000 revenue / 1000 paying users = $15 ARPPU. ### ARPU ARPU is the average revenue per user who viewed the onboarding. It's calculated as total revenue divided by the number of unique viewers. ### ARPAS ARPAS shows how much money each active subscriber generates on average. Simply divide your total revenue by your number of active subscribers. For example: $5,000 revenue ÷ 1,000 subscribers = $5 ARPAS. ### CR purchases & unique CR purchases **Conversion rate to purchases** shows what percentage of onboarding views lead to purchases. For example, 10 purchases from 100 views is 10% conversion rate. **Unique CR purchases** measures what percentage of unique users who view your onboarding end up making a purchase, counting each user only once, regardless of how many times they see it. ### CR trials & unique CR trials **Conversion rate to trials** shows what percentage of onboarding views lead to starting a trial. For example, 10 trials from 100 views is 10% conversion rate. **Unique CR trials** measures what percentage of unique users who view your onboarding start a trial, counting each user only once, regardless of how many times they see it. ### Purchases **Purchases** counts all transactions on your onboarding, except renewals. This includes: - New direct purchases - Trial conversions - Plan changes (upgrades, downgrades, cross-grades) - Subscription restores This metric gives you a complete picture of new transaction-related activity from your onboarding. ### Trials **Trials** counts the number of users who started free trial periods through your onboarding. This helps you track how well your trial offers attract users before they decide to pay. ### Trials cancelled **Trials cancelled** shows how many users turned off auto-renewal during their trial period. This tells you how many people decided not to continue with a paid subscription after trying your service. ### Refunds **Refunds** counts how many purchases and subscriptions were returned for a refund, regardless of the reason. ### Refund rate **Refund rate** shows the percentage of first-time purchases refunded. Example: 5 refunds from 1,000 purchases = 0.5% refund rate. Renewals aren't counted in this calculation. --- # File: onboarding-navigation-branching.md --- --- title: "Onboarding navigation" description: "Configure static and dynamic navigation in Adapty’s no-code onboarding builder to guide users through flows." --- Navigation and branching lets you guide users through every step of your onboarding: use static routes to send everyone to core screens, and dynamic navigation to adapt the flow based on user choices. All without writing a single line of code. ## Set up navigation You can configure static and dynamic navigation, as well as onboarding closure, using [buttons](onboarding-buttons.md) and [quizzes](onboarding-quizzes.md). :::info For quizzes, only single-answer quizzes are suitable for navigation. Multiple-answer quizzes can be used to set [conditional element visibility](onboarding-element-visibility.md). ::: ### Static navigation Static navigation directs users to the same target screen. To set it up: 1. Add a button or a single-answer quiz. 2. Select the button or quiz and go to the **Element** tab on the right. 3. Set up the **On Press** button section or **Behaviour** for the quiz: - **Action on** (for quiz only): Select **Option** to unlock navigation settings for the quiz. - **Action**: Select **Navigate**. - **Data**: Select **Static** to direct users to the same target screen. - **Destination**: Choose the destination screen. :::note With static navigation, a quiz directs users to the same screen regardless of the answer they select. ::: ### Dynamic navigation Dynamic navigation routes users based on their quiz answers: - **Quiz answers on previous screens**: Both buttons or single-answer quizzes can trigger navigation. - **Quiz answers on the current screen**: Only single-answer quizzes can trigger navigation. To set it up: 1. Add a button or a single-answer quiz that will navigate users. 2. Select the button or quiz and go to the **Element** tab on the right. 3. Set up the **On Press** button section or **Behaviour** for the quiz: - **Action on** (for quiz only): Select **Option** to unlock navigation settings for the quiz. - **Action**: Select **Navigate**. - **Data**: Select **Dynamic** to direct users based on their previous quiz answers. - **State**: Choose a quiz whose answers determine the user destination. 4. Select the destination screen for each quiz option. Your button or quiz will dynamically route users to the destinations you configured. ### Onboarding closure If your user journey calls for closing the onboarding flow, you can set it up using buttons or single-answer quizzes: 1. Add a button or a single-answer quiz. 2. Select the button or quiz and go to the **Element** tab on the right. 3. Set up the **On Press** button section or **Behaviour** for the quiz: - **Action on** (for quiz only): Select **Option** to unlock navigation settings for the quiz. - **Action**: Select **Close onboarding**. Learn how to handle onboarding closure on [iOS](ios-handling-onboarding-events.md#closing-onboarding), [Android](android-handle-onboarding-events.md#closing-onboarding), [Flutter](flutter-handling-onboarding-events.md#closing-onboarding), and [React Native](react-native-handling-onboarding-events.md#closing-onboarding). --- # File: onboarding-offline.md --- --- title: "Offline mode" description: "Handle trying to open an offboarding offline." --- An internet connection is required to fetch the onboarding flow from the server. This is essential for retrieving the onboarding data and displaying the onboarding sequence and the paywall with products that follow. Both onboarding and paywall content are dynamically loaded from the server, ensuring they are always up-to-date. ## Offline mode To optimize the user experience, the onboarding sequence is loaded this way: - **Initial screen load**: Only the first screen of the onboarding flow is required to be loaded initially. This allows us to minimize load times, even on slower mobile connections such as 3G or 4G. - **Preloading**: Once the first screen is loaded and displayed, we immediately start preloading the subsequent screens (including fonts, videos, images) in the background. If a user loses internet connectivity during the onboarding process, they will encounter an error screen with two options: - **Try again**: Upon tapping **Try again**, the system will reattempt to load the onboarding flow. If the connection is restored and the content is successfully loaded, the onboarding will resume from where the user left off, with all progress preserved. - **Close**: If the user decides to close the onboarding, the [close](ios-handling-onboarding-events#closing-onboarding) event with `"action_id": "error"` will be triggered. --- # File: onboarding-quizzes.md --- --- title: "Onboarding quizzes" description: "Add interactive quizzes to your Adapty onboardings to collect user preferences and drive personalized flows—no code needed." --- Turn your onboarding into a two‑way conversation: add quizzes in Adapty's no‑code builder to collect preferences, segment users, and [branch flows](onboarding-user-engagement.md#onboarding-flow-branching) based on real‑time answers. You'll be gathering insights in minutes—no code required. ## Add quizzes You can add various quiz types—text, emoji, or image options—to gather user input: 1. Click **Add** at the top left. 2. Select **Quiz** and choose one: - **Simple**: A single-select list of text options. Use to segment users by a primary attribute (e.g., “What’s your role?”). - **Multiple choice**: Allows selecting more than one text option. Ideal for gathering all user interests (e.g., favorite features). - **Emoji**: Options represented by emojis for quick reactions. Great for fast sentiment checks (e.g., “How excited are you?”). - **Media picker**: Apload images or videos as selectable choices. Perfect for choices that rely on visuals (e.g., select your favorite theme). - **Rating**: Users rate on a numerical or star scale. Use to measure satisfaction or confidence (e.g., rate this feature 1–5). - **Popup question**: Displays a modal question overlay. Excellent for time-sensitive prompts. 3. Set up the quiz on the right: - **Required**: Make an answer mandatory before users can proceed. - **Layout**: Choose between list or image tile layouts. - **Multiple answers**: Allow multi-select (disables navigation options for the quiz). - **Show checkboxes**: Display checkboxes when multiple answers are enabled. 4. Set up quiz options on the right: - **Label**: Text displayed for each choice. - **Value**: The value sent to analytics and webhook payloads. - **Image type**: Upload media or use emojis. 5. Configure [actions](onboarding-actions.md) to fire when the user selects an option. Learn more in the [guide on designing quizzes](#how-to-design-quizzes) below. ### How to design quizzes Here's a simple quiz setup example. Let's say you have a recipe app and want to know if your users are vegan or vegetarian, then learn more about their preferences based on their answer. #### Step 1. Add screens 1. Add a new screen and add a **Quiz** element to it. 2. Add screens for different user groups. In our example, these will collect additional information, so they'll also contain quizzes. 3. Add a final screen to indicate the onboarding is complete, allowing users to go straight to the app. #### Step 2. Configure navigation 1. To set up dynamic navigation, select the **Options** element on the first quiz screen. In the **Behavior** section, add **Action on Option**. Since we want to redirect users to different screens based on their answers, select **Navigate** as the action, choose **Dynamic** for **Data**, and select your **Options** element in **State**. Then associate each option with a screen. 2. On both conditional screens, configure the navigation button. Since we need to skip the second conditional screen, point the navigation button directly to the screen you want to show next. :::tip If you want to customize the onboarding itself based on the quiz answers, see the guide for [navigation](onboarding-navigation-branching#dynamic-navigation) or for [using variables](onboarding-variables.md). ::: ## Quiz customization Beyond the basic [element layout](onboarding-layout.md#element-layout), you can customize quiz appearance: 1. Select the quiz element on the left. 2. Go to **Styles** in the right menu. 3. Adjust these settings: - **Options**: Height, padding, background, roundness, border, border color. - **Text**: Color, font, alignment. - **Pressed State**: Background, text color, border color. :::tip After customizing a quiz element, you can click **Apply styles to all options** below to apply the same styles across all onboarding screens in bulk. ::: ## Save quiz answers You can also process the quiz answers in your app and store them or use them in your application. To do this, you must handle the quiz response event in the app code. See the guide for your platform: - [iOS](ios-handling-onboarding-events#updating-field-state) - [Android](android-handle-onboarding-events#updating-field-state) - [React Native](react-native-handling-onboarding-events#updating-field-state) - [Flutter](flutter-handling-onboarding-events#updating-field-state) --- # File: onboarding-text.md --- --- title: "Onboarding text" description: "Add and style titles, subtitles, paragraphs, and lists in Adapty’s onboarding builder, and customize text for on-brand user experiences." --- Text elements help you create clear, personalized conversations with your users. Add titles, paragraphs, or lists with a single click, style them to match your brand, and use [dynamic variables](onboarding-variables.md) to personalize content for each user. ## Add text You can add various text elements to your onboarding screens. To add text elements: 1. Click **Add** at the top left. 2. Go to **Typography** and choose one: - **Title**: hero headlines or screen titles that instantly grab attention. - **Subtitle**: a short supporting line that expands on the title. - **Text**: body copy for feature descriptions, disclaimers, or inspirational blurbs. - **Rich text**: mixed formatting for FAQs, terms of service, or any copy that needs links and emphasis. 3. Click the new element to edit its content. 4. (Optional) Select any part of the text to open a tooltip for quick customization—such as bold, italic, links, text color, or resetting styles. To edit an existing text element, simply click on it and make changes in WYSIWYG mode. :::tip If you need to use the same text element on multiple screens, you can copy and paste it: select the element and press Ctrl+C (or ⌘+C on Mac), navigate to another screen, select the element you want to paste after, and press Ctrl+V (or ⌘+V on Mac). ::: ## Add lists You can add numbered and bullet lists: 1. Click **Add** at the top left. 2. Go to **Typography** and choose one: - **Numbered list**: perfect for step‑by‑step guides. - **Bullet list**: highlight benefits or key features without implying order. 3. Go to the **Element** tab on the right to edit list items or upload an image as an item marker. To edit an existing list element, click on it and make changes in the **Element** tab. ## Text & list customization In addition to the basic [element layout](onboarding-layout.md#element-layout), you can customize the appearance of text and lists: 1. Select the element on the left. 2. Go to **Styles** on the right menu. 3. Based on the element type, you can adjust the following options: - **Text**: Paragraph color, font, alignment, and line height, links color, font, and decoration. - **List**: Text and marker text color and font, marker image width, height, and roundness. :::tip To speed things up: - After customizing a text element, you can click **Apply styles to all paragraphs** below to apply the same styles across all onboarding screens in bulk. - To change the font for all text elements on a specific screen, select the screen, then go to **Styles > Text** on the right menu. ::: ## Fonts In the onboarding builder, you can select from a big variety of different fonts. :::info Uploading custom fonts is not available yet. ::: You can set fonts globally for the whole onboarding or for each its element separately: - To set up the main font that will be used in the onboarding: 1. Select any screen on the left. 2. Switch to the **Styles** tab and select a **Font**. 3. All the elements on all the screens will inherit the font you've selected. - To set up a font for one element: 1. Select an element. 2. Switch to the **Styles** tab and select a **Font**. 3. The selected font will be used for this element even if you change the main font. :::note You can't use SF Pro, because it's not suitable for cross-platform applications, but we recommend you to use Inter instead, since they look quite similar. ::: --- # File: onboarding-user-engagement.md --- --- title: "Customize onboardings for different user groups" description: "Customize onboardings for different user groups and branch flows based on their choices." --- Adapty onboardings offer many options to customize experiences for different user groups: - [Quizzes](onboarding-quizzes.md): Collect user preferences and real-time answers. - [Dynamic navigation](onboarding-navigation-branching.md#dynamic-navigation): Route users based on their previous quiz answers or behavior. - [Conditional visibility](onboarding-navigation-branching.md#element-visibility): Show or hide elements on the same screen without navigating away. - [Variables](onboarding-variables.md): Personalize your communication with users based on the data they input. - [Actions](onboarding-actions.md): Assign interactive behaviors to onboarding elements to control how users move through and interact with your flow. These features let you adjust the same flow for different users or create branched onboarding flows. ## Onboarding flow branching Branched onboarding flows split users into separate paths within a single onboarding, delivering tailored content based on their responses or behavior. For example, here's how you could branch flows in a recipe app: 1. Add a new screen with a [quiz](onboarding-quizzes.md). Each option represents a user group. 2. Set up different next screens for each group. These screens can include another quiz to gather more data. 3. Set up [dynamic navigation](onboarding-navigation-branching.md#dynamic-navigation) so quiz answers direct each group to the appropriate screen. 4. Use [conditional elements](onboarding-navigation-branching.md#element-visibility) on the final screen to show different visuals for each user group. --- # File: onboarding-variables.md --- --- title: "Onboarding variables" description: "Use dynamic variables in Adapty’s no-code onboarding builder to personalize content, capture user data, and drive tailored user flows." --- Variables are values set based on user input or environmental data. They're essential for creating personalized and engaging onboarding experiences. ## What variables are for Variables let you insert dynamic data—like quiz responses or user text inputs—directly into your onboarding screens. Each user sees personalized content without any coding required. For example, greet users by name using text inputs, or route quiz responders to custom follow-up screens. You use variables by placing the element ID of the data source between double braces, like this: `{{element_id}}`. As variables, you can use the data collected on previous screens: - **Inputs**: The variable contains data entered by the user. - [**Quizzes**](onboarding-quizzes.md): The variable contains the label data of selected options. If multiple answers are allowed, the variable will contain all selected options, separated by a comma and space. :::note Element ID is not the same as the [action ID](onboarding-actions.md#action-id) used for custom actions logic. Be sure not to mix them up. ::: ## Use variables Here's how to use variables: 1. Create an Input element or quiz option and set its ID. 2. Use the element ID in onboarding texts in the `{{element-id}}` format. For example, you can personalize your text using the user's name. 3. When users enter their data during onboarding, it will appear dynamically wherever you've placed variables. --- # File: onboardings.md --- --- title: "Onboardings" --- Adapty's onboardings let your non-technical teams create attractive and customizable onboarding flows without coding. Our no-code builder helps you design a series of screens that guide users through their first app experience. :::important Onboardings are available only for apps using Adapty iOS, Android, Flutter, and React Native SDK version 3.8.0 or higher. ::: ## What it is for Good onboarding introduces users to your app by: - Showing your app's core value - Explaining key features and functionality - Providing essential usage tips Adapty's onboarding solution stands out with: - No-code [onboarding builder](design-onboarding.md) that empowers non-technical teams - The ability to [personalize experiences through interactive questions and variables](onboarding-user-engagement.md) - A/B testing support to determine which onboarding flows perform best ## Pricing Onboardings are a paid feature in Adapty. Note the following about the pricing: - Using onboardings costs 0.2% of combined monthly revenue from all your apps. - You can test onboardings in the sandbox freely. You start getting billed only after the first transaction in the production environment. ## How it works To launch your onboarding: 1. [Design an onboarding in the no-code editor.](design-onboarding.md) 2. [Create a placement for the onboarding.](create-onboarding#create-a-placement-for-your-onboarding) 3. Integrate the onboarding with your project using the Adapty SDK: - [iOS](ios-onboardings.md) - [Android](android-onboardings.md) - [Flutter](flutter-onboardings.md) - [React Native](react-native-onboardings.md) 4. Test the onboarding and release it for your users. To grow further, you can also try more advanced ways to work with onboardings: - Add more audiences to the placement to show different onboardings to different user groups. - Run A/B tests of onboardings to find the most efficient option. - Connect onboardings to paywalls and increase the conversion. --- # File: onesignal.md --- --- title: "OneSignal" description: "Integrate OneSignal with Adapty to improve push notification-based engagement." --- [OneSignal](https://onesignal.com/) is a leading customer engagement platform offering push notifications, email, SMS, and in-app messaging. Integrating Adapty with OneSignal enables you to access all your subscription events in one place, allowing you to trigger automated communication based on those events. With Adapty, you can track [subscription events](events) across multiple stores, analyze user behavior, and use that data for more targeted communication. This integration helps you monitor subscription events within your OneSignal dashboard and map them to your [acquisition campaigns](https://documentation.onesignal.com/docs/automated-messages#example-automated-message-campaigns). Adapty updates OneSignal tags based on subscription events, enabling you to deliver personalized push notifications with minimal setup. **Integration characteristics** | Integration characteristic | Description | | :------------------------- | :----------------------------------------------------------- | | Schedule | Real-time updates | | Data direction | One-way: from Adapty to OneSignal server | | Adapty integration point |
  • OneSignal and Adapty SDKs in the mobile app code
  • Adapty server
| ## Setting up One Signal integration To set up the integration: 1. Open [Integrations → OneSignal](https://app.adapty.io/integrations/onesignal) in your Adapty Dashboard. 2. Enable the integration toggle. 3. Enter your **OneSignal App ID**. To set up the integration with OneSignal, go to [Integrations -> OneSignal](https://app.adapty.io/integrations/onesignal) in your Adapty dashboard, turn on a toggle, and configure the integration credentials. ## Retrieving your OneSignal App ID Find your **OneSignal App ID** in your [OneSignal Dashboard](https://dashboard.onesignal.com/login): 1. Navigate to **Settings** → **Keys & IDs**. 2. Copy your **OneSignal App ID** and paste it into the **App ID** field in the Adapty Dashboard. You can find more information about the OneSignal ID in the [following documentation.](https://documentation.onesignal.com/docs/keys-and-ids) ### Configuring events Adapty allows you to send three groups of events to OneSignal. Toggle on the ones you need in the Adapty Dashboard. You can view the complete list of available events with detailed description [here](events). Adapty sends subscription events to OneSignal using a server-to-server integration, allowing you to track all subscription-related activity in OneSignal. :::warning Starting April 17, 2023, OneSignal's Free Plan no longer supports this integration. It is available only on **Growth**, **Professional**, and **higher** plans. For details, see [OneSignal Pricing](https://onesignal.com/pricing). ::: ## Custom tags This integration updates and assigns various properties to your Adapty users as tags, which are then sent to OneSignal. Refer to the list of tags below to find the ones that best fit your needs. :::warning OneSignal has a tag limit. This includes both Adapty-generated tags and any existing tags in OneSignal. Exceeding the limit may cause errors when sending events. ::: | Tag | Type | Description | |---|----|-------------------------------------------------------------------------------------------------------------------------------------------------| | `adapty_customer_user_id` | String | The unique identifier of the user in your app. It must be consistent across your system, Adapty, and OneSignal. | | `adapty_profile_id` | String | The Adapty user profile ID, available in your [Adapty Dashboard](profiles-crm). | | `environment` | String | `Sandbox` or `Production`, indicating the user’s current environment. | | `store` | String | Store where the product was bought. Options: **app_store**, **play_store**, **stripe**, or the name of your [custom store](custom-store). | | `vendor_product_id` | String | The product ID in the app store (e.g., `org.locals.12345`). | | `subscription_expires_at` | String | Expiration date of the latest subscription (`YYYY-MM-DDTHH:MM:SS+0000`, e.g., `2023-02-10T17:22:03.000000+0000`). | | `last_event_type` | String | The latest event type from the [Adapty event list](events). For the **Subscription expired** event, Adapty sends the `last_event_type` property as `subscription_cancelled`. | | `purchase_date` | String | Last transaction date (`YYYY-MM-DDTHH:MM:SS+0000`, e.g., `2023-02-10T17:22:03.000000+0000`). | | `active_subscription` | String | `true` if the user has an active subscription and `false` if the subscription has expired. | | `period_type` | String | Indicates the most recent period type for the purchase or renewal. Possible values: `trial` for a trial period or `normal` for all other cases. | All float values are rounded to integers. The strings remain unchanged. In addition to the predefined tags, you can send [custom attributes](segments#custom-attributes) as tags, providing greater flexibility in the data you include. This is useful for tracking specific details related to your product or service. Custom user attributes are automatically sent to OneSignal if the **Send user attributes** checkbox is enabled on the [integration page](https://app.adapty.io/integrations/onesignal). When unchecked, Adapty sends exactly 10 tags. If checked, more than 10 tags can be sent, allowing for enhanced data capture. ### SDK configuration There are two ways to integrate OneSignal with Adapty: 1. **Legacy (pre-v5):** Uses `playerId` (deprecated in [OneSignal SDK v5](https://github.com/OneSignal/OneSignal-iOS-SDK/releases/tag/5.0.0)). 2. **Current (v5+):** Uses `subscriptionId`. :::warning Make sure to send `playerId` (for OneSignal SDK pre-v5) or `subscriptionId` (for OneSignal SDK v5+) to Adapty. Without this, OneSignal tags won’t be updated, and the integration won’t function properly. ::: ```swift showLineNumbers // SubscriptionID OneSignal.Notifications.requestPermission({ accepted in Task { try await Adapty.setIntegrationIdentifier( key: "one_signal_subscription_id", value: OneSignal.User.pushSubscription.id ) } }, fallbackToSettings: true) ``` ```kotlin showLineNumbers // SubscriptionID val oneSignalSubscriptionObserver = object: IPushSubscriptionObserver { override fun onPushSubscriptionChange(state: PushSubscriptionChangedState) { Adapty.setIntegrationIdentifier("one_signal_subscription_id", state.current.id) { error -> if (error != null) { // handle the error } } } } ``` ```java showLineNumbers // SubscriptionID IPushSubscriptionObserver oneSignalSubscriptionObserver = state -> { Adapty.setIntegrationIdentifier("one_signal_subscription_id", state.getCurrent().getId(), error -> { if (error != null) { // handle the error } }); }; ``` ```javascript 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()); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } } }); ``` ```csharp showLineNumbers using AdaptySDK; using OneSignalSDK; var pushUserId = OneSignal.Default.PushSubscriptionState.userId; Adapty.SetIntegrationIdentifier( "one_signal_player_id", pushUserId, (error) => { // handle the error }); ``` ```typescript showLineNumbers OneSignal.User.pushSubscription.addEventListener('change', (subscription) => { const subscriptionId = subscription.current.id; if (subscriptionId) { adapty.setIntegrationIdentifier("one_signal_subscription_id", subscriptionId); } }); ``` ```swift showLineNumbers // PlayerID // in your OSSubscriptionObserver implementation func onOSSubscriptionChanged(_ stateChanges: OSSubscriptionStateChanges) { if let playerId = stateChanges.to.userId { Task { try await Adapty.setIntegrationIdentifier( key: "one_signal_player_id", value: playerId ) } } } ``` ```kotlin showLineNumbers // PlayerID val osSubscriptionObserver = OSSubscriptionObserver { stateChanges -> stateChanges?.to?.userId?.let { playerId -> Adapty.setIntegrationIdentifier("one_signal_player_id", playerId) { error -> if (error != null) { // handle the error } } } } ``` ```java showLineNumbers // PlayerID OSSubscriptionObserver osSubscriptionObserver = stateChanges -> { OSSubscriptionState to = stateChanges != null ? stateChanges.getTo() : null; String playerId = to != null ? to.getUserId() : null; if (playerId != null) { Adapty.setIntegrationIdentifier("one_signal_player_id", playerId, error -> { if (error != null) { // handle the error } }); } }; ``` ```javascript showLineNumbers // PlayerID (pre-v5 OneSignal SDK) // in your OSSubscriptionObserver implementation func onOSSubscriptionChanged(_ stateChanges: OSSubscriptionStateChanges) { if let playerId = stateChanges.to.userId { Task { try await Adapty.setIntegrationIdentifier( key: "one_signal_player_id", value: playerId ) } } } ``` ```typescript showLineNumbers OneSignal.addSubscriptionObserver(event => { const playerId = event.to.userId; adapty.setIntegrationIdentifier("one_signal_player_id", playerId); }); ``` Read more about `OSSubscriptionObserver` in the [OneSignal documentation](https://documentation.onesignal.com/docs/sdk-reference#handling-subscription-state-changes). ## Dealing with multiple devices If a user has multiple devices, tracking purchase events and subscriptions can be challenging. OneSignal provides a way to handle this through [external user IDs](https://documentation.onesignal.com/docs/external-user-ids). To keep user data consistent across devices: 1. Match different devices on your **server side** and send this data to OneSignal. 2. Use Adapty’s [customer_user_id](identifying-users) as an [externalUserId](https://documentation.onesignal.com/docs/external-user-ids#setexternaluserid-method) in OneSignal. If your app doesn't have a registration system, consider using another unique identifier that remains consistent across the user's devices. It's important to maintain consistency in the user identifier across all devices and update OneSignal whenever a user's ID changes. This simplifies tracking user activity and subscriptions while ensuring consistent messaging and allows for more accurate analytics and a better user experience. For more details, see OneSignal's [external user ID documentation](https://documentation.onesignal.com/docs/external-user-ids). --- # File: overview.md --- --- title: "Analytics overview" description: "Explore the Adapty documentation overview for insights into subscription optimization." --- [Overview](https://app.adapty.io/overview) is a section of Adapty Dashboard that allows you to see multiple metrics in a single place. You can customize which charts you would like to see and view data for all of your apps at once (unlike in [Charts](charts) which you can use to dive deeper into a particular app). It's located right under Home in the left-side menu: ## Charts Overview has the following charts available (you can click on the name to learn more about how we calculate it): - [Revenue](revenue) - [MRR](mrr) - [ARR](arr) - [ARPU](arppu.md) - [ARPPU](arppu) - [ARPAS](https://adapty.io/docs/placement-metrics#arpas) - [Installs](installs.md) - [New trials](new-trials) - [New subscriptions](reactivated-subscriptions) - [Active trials](active-trials) - [Active subscriptions](active-subscriptions) - [New non-subscriptions](non-subscriptions) - [Refund events](refund-events) - [Refund money](refund-money) - [Subscriptions renewal canceled](cancelled-subscriptions.md) - [Conversion rate from Install to Trial, Install to Paid, and Trial to Paid](analytics-conversion.md) You can customize which charts to show as well as their order. To do that, press Edit in the top-right corner and then either remove charts you don't need, add more or rearrange existing ones by drag and dropping. You can also customize Overview contents in the "Add" menu: ## Controls Controls for diving deeper into your data in Overview are very similar to what we have in [Charts](charts) — and most of them are described in [Analytics controls](controls-filters-grouping-compare-proceeds). There is one important difference though: you can group and filter by country, store, and, most notably, by app — as Overview shows data for all of your apps at once by default. This can be helpful to understand how each of your app contributes to your business metrics: :::note **Timezone and install settings** Note that these settings apply to all your apps and override what you have in [App settings](general). - **Installs**: By default, installs are counted by `device_id`—a new installation or reinstallation on a device is counted as a separate install. You can change it by clicking **Edit**. For a detailed explanation of other options, see the [Installs definition for analytics](general#4-installs-definition-for-analytics) section. - **Timezone**: By default, the timezone for the **Overview** page is inherited from one of your apps. If your apps have different reporting timezones, customize the Overview timezone by clicking **Edit** and selecting the appropriate option from the dropdown. ::: --- # File: paddle.md --- --- title: "Initial integration with Paddle" description: "Integrate Paddle with Adapty for seamless subscription payment processing." --- Adapty lets you manage subscriptions and purchases from both your mobile app and website in one place. The [Paddle](https://www.paddle.com/) integration enables you to: - Collect subscription data from both in-app purchases and website purchases in a single system - Grant access to paid features in your mobile app to users who purchased on your website - View analytics and subscription data from all sales channels in one dashboard :::note Apple now allows US App Store apps to include links to external payment systems, though apps may still need to offer in-app purchases alongside external options. Check the current App Store guidelines for your region and app category. ::: To set up the Paddle integration, follow these steps: ## 1\. Connect Paddle to Adapty The integration uses webhooks to send subscription data from Paddle to Adapty. To connect your Adapty and Paddle accounts, you'll need to: 1. Provide your Paddle API keys. 2. Add Adapty's webhook URL to Paddle. :::note The steps below apply to both Production and Test. You can configure both simultaneously. The links provided are for the Production environment — to get the Test environment links, simply add `sandbox-` at the beginning of each URL. For example, use `https://sandbox-vendors.paddle.com/authentication-v2` instead of `https://vendors.paddle.com/authentication-v2`. ::: ### 1.1. Get and add Paddle API keys 1. In Paddle, go to [Developer Tools → Authentication](https://vendors.paddle.com/authentication-v2) and click **New API key**. 2. Give the key a name and set the expiration date. For the API key to work with Adapty, you need to grant it the **Read** permission for all entities. Click **Save**. 3. Click **Copy key**. 4. In Adapty, go to [App Settings → Paddle](https://app.adapty.io/settings/paddle) and paste the key in the **Paddle API key** section. :::warning If you set an expiration date for your Paddle API key, you must manually generate a new key and update it in Adapty before expiration. The integration will stop working without warning when the key expires, and users won't be able to make purchases. ::: ### 1.2. Add events that will be sent to Adapty 1. Copy the **Webhook URL** from the same **Paddle** page in Adapty. 2. In Paddle, go to [**Developer Tools → Notifications**](https://vendors.paddle.com/notifications-v2) and click **New destination** to add a webhook. 3. Enter a descriptive name for the webhook. We recommend including "Adapty" in it, so you can easily find it when needed. 4. Paste the **Webhook URL** from Adapty into the **URL** field. Ensure you are using the webhook for the right environment. 5. Set **Notification type** to **Webhook**. 6. Select the following events: - `subscription.created` - `subscription.updated` - `transaction.created` - `transaction.updated` - `adjustment.created` - `adjustment.updated` 7. Click **Save destination** to finalize the webhook setup. ### 1.3. Retrieve and add the webhook secret key 1. In the **Notifications** window, click the three dots next to the webhook you just created and select **Edit destination**. 2. A new field called the **Secret key** will appear in the **Edit destination** panel. Copy it. 3. In Adapty, go to [App Settings → Paddle](https://app.adapty.io/settings/paddle) and paste the key into the **Notification secret key** field. This key is used to verify webhook data in Adapty. ### 1.4. Match Paddle customers with Adapty profiles Adapty needs to link each purchase to a [customer profile](profiles-crm) so it can be used in your app. By default, profiles are created automatically when Adapty receives webhooks from Paddle. You can choose which value to use as the `customer_user_id` in Adapty: 1. **Default and recommended:** The `customer_user_id` you pass in the `custom_data` field (see [Paddle docs](https://developer.paddle.com/build/transactions/custom-data)) 2. The `email` from the Paddle Customer object (see [Paddle docs](https://developer.paddle.com/paddlejs/methods/paddle-checkout-open#params)) 3. The Paddle Customer ID in the `ctm-...` format (see [Paddle docs](https://developer.paddle.com/paddlejs/methods/paddle-checkout-open#params)) 4. Don't create profiles. Choose this option if you want to have more control over your customer profiles and handle it yourself. You can configure which value to use in the **Profile creation behavior** field in [App Settings → Paddle](https://app.adapty.io/settings/paddle). ## 2. Add Paddle products to Adapty :::warning Be sure to add your Paddle products to the Adapty Dashboard or add a Paddle product ID to your existing products. Adapty only tracks events for transactions tied to these products. If you skip this step, transaction events won't be created. ::: Paddle works in Adapty just like App Store and Google Play — it's another platform where you sell digital products. To configure it, add the relevant `product_id` and `price_id` values from Paddle in the [Products](https://app.adapty.io/products) section in Adapty. In Paddle, product IDs look like `pro_...` and price IDs like `pri_...`. You'll find them in your [Paddle product catalog](https://vendors.paddle.com/products-v2) once you open a specific product: Once your products are added, the next step is ensuring Adapty can link the purchase to the right user. ## 3\. Provide access to users on the mobile To ensure users who buy on the web get access on mobile, call `Adapty.activate()` or `Adapty.identify()` using the same `customer_user_id` you passed when the purchase was made. See [Identifying users](identifying-users) for details. ## 4\. Test your integration Once everything's set up, you can test your integration. Transactions made in Paddle's Test environment will appear as **Test** in Adapty. Transactions from the Production environment will appear as **Production**. :::note In Adapty's analytics, transaction amounts include taxes and Paddle fees, which differs from Paddle's dashboard where amounts are shown after taxes and fees. This means the numbers you see in Adapty will be higher than those in your Paddle dashboard. ::: :::note Unlike other stores, refunds in Paddle only affect the specific transaction being refunded and do not automatically cancel the subscription. The subscription will continue to be active unless explicitly canceled. ::: Your integration is now complete. Users can purchase subscriptions on your website and automatically access premium features in your mobile app, while you track all subscription analytics from your unified Adapty dashboard. :::tip You can also include `variation_id` in the `custom_data` field to attribute purchases to specific paywall instances. Adapty will process this data from webhooks and include it in analytics. ::: ## Current limitations - **Cancellations**: Paddle has two subscription cancellation options: 1. Immediate cancellation: The subscription is canceled immediately. 2. Cancellation at the end of the period: The subscription cancels at the end of the current billing period (similar to in-app subscriptions on the app stores). - **Refunds**: Adapty tracks full and partial refunds. - **Grace period**: Paddle has a fixed grace period of 30 days for billing issues, which cannot be customized. During this period, the subscription remains active even if there are billing issues. This means users will continue to have access to premium features for the full grace period duration, regardless of payment status. Note that for trials, there is no grace period - if there is a billing issue after a trial, the subscription will be cancelled immediately. --- **See also:** - [Validate purchase in Paddle, get access level, and import transaction history from Paddle with server-side API](ss-validate-paddle-token) --- # File: payment-integrations.md --- --- title: "Web" description: "Integrate web payment systems with Adapty for seamless subscription management across platforms." --- Adapty supports integration with web payment systems to help you manage subscriptions and purchases from both your mobile app and website in one place. This enables you to: - Collect subscription data from both in-app purchases and website purchases in a single system - Grant access to paid features in your mobile app to users who purchased on your website - View analytics and subscription data from all sales channels in one dashboard Available integrations: - [Stripe](stripe) - [Paddle](paddle) --- # File: paywall-builder-tag-variables-legacy.md --- --- title: "Tag variables for product info in legacy Paywall Builder" description: "Use tag variables in Adapty’s legacy Paywall Builder to optimize offers." --- Adapty Paywall Builder has a way to customize all the text corresponding to your products and their offers. But if you have multiple locales — we strongly recommend using variables. ### How it works When texts for your products contain a tag variable from our list, our SDK uses the pre-fetched localized data from the stores to put it in place of a tag. That way the text on your paywall is always tailored for the right locale. **Example**: suppose you have a "Premium Subscription" and your app is available both in the US and Spain. So you're selling "Premium Subscription for $4.99/month" in the US and "Suscripción Premium por €4.99/mes" in Spain. Tag variables allow you to rely on the data obtained directly from the store to localize such strings — so titles and prices will always be correct. :::warning This section describes the legacy Paywall Builder, compatible with Adapty SDK v2.x or earlier. For information on the new Paywall Builder compatible with Adapty SDK v3.0 or later, see [Tag variables for product info in new Paywall Builder](paywall-builder-tag-variables). ::: ### How to use tag variables :::note You can only use tag variables when describing products and offers in the "Products" tab of the Paywall Builder ::: 1. Select the "Products" tab of the Builder 2. Choose the product you'd like to customize: 3. Use variables from [the table below](paywall-builder-tag-variables#full-list-of-variables) in any of the text fields to describe the product and its offers: 4. Check Preview on the right side of the screen to make sure all renders as intended. :::warning **Note:** Preview doesn't use any real values to put in place of your variables as they are only obtained on a device by our SDK. However, by default it displays some template data that's in the same format as the real result. You can disable this behavior by pressing "eye" icon in the top-right corner of the Preview and switching off the **"Tags preview values"** switch. The preview will then show the actual values of the variables: ::: ### Full list of variables | Tag variable | Description | Example | | :------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------- | | `` | Localized title of the product | Premium Subscription | | `` | Localized price of the product. In case of subscriptions the price is for one billing period. | $9.99 | | `` | Price of a subscription divided by the number of days in the billing period. **Returns empty string for non-subscriptions.** | $0.33 | | `` | Price of a subscription divided by the number of weeks in the billing period. **Returns empty string for non-subscriptions.** | $2.33 | | `` | Price of a subscription divided by the number of months in the billing period. If actual billing period is less than a month — it's multiplied to represent how much a user would pay for a full month.**Returns empty string for non-subscriptions.** | $9.99 | | `` | Price of a subscription that represents how much a user would pay for a full year of usage. So, for example, the price for the monthly subscription would be multiplied by 12 and the price for the yearly one would remain the same. **Returns empty string for non-subscriptions.** | $119.88 | | `` | Localized price of an offer (intro or promo). **Applicable only to auto-renewable subscriptions, returns empty string if user is not eligible for any offers** | $0.99 | | `` | Localized billing period of an offer (intro or promo). Same as ``for trial and pay-upfront offers. **Applicable only to auto-renewable subscriptions, returns empty string if user is not eligible for any offers** | 1 week | | `` | Localized full duration of an offer (intro or promo). **Applicable only to auto-renewable subscriptions, returns empty string if user is not eligible for any offers** | 1 month | ### Offer tags for different offer types :::note You can learn more about Offers and how you can configure them in Adapty [here](offers) ::: Offer tags for different offer types might be confusing, so let's consider an example. Suppose we have a weekly subscription called "Premium Subscription" for a price of $5. For it we have 3 possible offers: - **Pay As You Go**. First 3 weeks for a price of $3 (billed each week), then $5/week - **Pay Up Front**. First 3 weeks for a price of $8 (billed right now), then $5/week - **Free Trial**. First week free, then $5/week. `` for this product would be "Premium Subscription" and its `` would be $5. But the values for the offer tags — depending on which offer the user is eligible for — would be: | Tag variable | Pay As You Go | Pay Upfront | Free Trial | | :------------------------ | :------------ | :---------- | :--------- | | `` | $3 | $8 | $0 | | `` | 1 week | 3 weeks | 1 week | | `` | 3 weeks | 3 weeks | 1 week | So in any offer other than a "Pay As You Go" type,`` and ``would be the same. --- # File: paywall-builder-tag-variables.md --- --- title: "Tag variables for product info in Paywall Builder" description: "Use tag variables in Adapty’s Paywall Builder to personalize user experiences and boost sales." --- Adapty’s Paywall Builder lets you customize all the text for your products and their offers. If you’re supporting multiple locales, we strongly recommend using variables. ### How it works When you add tag variables from our list to your product texts, our SDK pulls in the pre-fetched localized data from the app stores to replace the tags. This ensures that the text on your paywall is always perfectly tailored for the correct locale. **Example**: Let’s say you have a "Premium Subscription" available in both the US and Spain. In the US, it might display as "Premium Subscription for $4.99/month," while in Spain, it would show "Suscripción Premium por €4.99/mes." Tag variables allow you to automatically localize these strings based on data directly from the store, ensuring that titles and prices are always accurate. :::warning This section describes the new Paywall Builder, which works with iOS, Android, and React Native SDKs version 3.0 or higher, and Flutter and Unity SDKs version 3.3.0 or higher. For information on the legacy Paywall Builder compatible with Adapty SDK v2.x or earlier, see [Legacy Paywall Builder tag variables](paywall-builder-tag-variables-legacy). ::: ### How to use tag variables :::note You can only use tag variables when describing products and offers in the Product component of the Paywall Builder. ::: 1. In the Paywal Builder’s left pane, select the product you want to customize. 2. Use variables from the [table below](paywall-builder-tag-variables#full-list-of-variables) in any text fields to describe the product and its offers. 4. Check the preview on the right side of the screen to ensure everything renders as expected. :::note The preview doesn’t use real values to replace your variables; those are only retrieved by our SDK on a device. However, it does display template data in the same format as the actual result. You can disable this behavior by clicking the eye icon in the bottom-right corner of the preview and turning off the **Tags preview values** toggle. The preview will then show the actual values of the variables: ::: ### Full list of variables | Tag variable | Description | Example | | :------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------- | | `` | Localized title of the product | Premium Subscription | | `` | Localized price of the product. For subscriptions, this is the price for one billing period. | $9.99 | | `` | Subscription price divided by the number of days in the billing period. **Returns empty string for non-subscriptions.** | $0.33 | | `` | Subscription price divided by the number of weeks in the billing period. **Returns empty string for non-subscriptions.** | $2.33 | | `` | Subscription price divided by the number of months in the billing period. If the billing period is less than a month, it’s adjusted to represent a full month.**Returns empty string for non-subscriptions.** | $9.99 | | `` | Subscription price adjusted to represent a full year of usage. For example, the monthly subscription price would be multiplied by 12, while the yearly price remains the same. **Returns empty string for non-subscriptions.** | $119.88 | | `` | Localized price of an offer (intro or promo). **Applicable only to auto-renewable subscriptions, returns empty string if user is not eligible for any offers** | $0.99 | | `` | Localized billing period of an offer (intro or promo). Same as ``for trial and pay-upfront offers. **Applicable only to auto-renewable subscriptions, returns empty string if user is not eligible for any offers** | 1 week | | `` | Localized full duration of an offer (intro or promo). **Applicable only to auto-renewable subscriptions, returns empty string if user is not eligible for any offers** | 1 month | ### Offer tags for different offer types :::note You can learn more about Offers and how to configure them in Adapty [here](offers) ::: Understanding offer tags can be a bit tricky, so let’s break it down with an example. Suppose you have a weekly subscription called "Premium Subscription" for $5, with three possible offers: - **Pay As You Go**. First 3 weeks for $3 (billed weekly), then $5/week. - **Pay Up Front**. First 3 weeks for $8 (billed immediately), then $5/week. - **Free Trial**. First week free, then $5/week. For this product, `` would be "Premium Subscription" and `` would be $5. However, the values for the offer tags—depending on which offer the user is eligible for—would be: | Tag variable | Pay As You Go | Pay Upfront | Free Trial | | :------------------------ | :------------ | :---------- | :--------- | | `` | $3 | $8 | $0 | | `` | 1 week | 3 weeks | 1 week | | `` | 3 weeks | 3 weeks | 1 week | So for offers other than "Pay As You Go" type,`` and ``will be the same. --- # File: paywall-builder-templates-legacy.md --- --- title: "Legacy Paywall Builder templates" description: "Use legacy Paywall Builder templates in Adapty to streamline your monetization." --- [Adapty Paywall Builder](adapty-paywall-builder-legacy) simplifies the process of creating paywalls—specialized screens within your app where users can make purchases. This tool eliminates the need for technical expertise or design skills. You can effortlessly customize how your paywalls look, the messages they convey, and where essential buttons are positioned. What's more, you can even make real-time changes to these screens while your app is running — without App Store/Google Play reviews. Moreover, Adapty empowers you to optimize your paywalls further with [A/B testing](ab-tests). Alongside the Paywall Builder, this allows you to test different variations of your paywalls to find the most effective design and messaging. Whether you're striving to increase sales, promote content, or grant access to exclusive features, the Paywall Builder provides a user-friendly solution to accomplish these objectives. When creating or editing paywalls using Adapty's Paywall Builder, you have the flexibility to choose from three distinct layout options, each with its own corresponding templates. The template layout you select will dictate the visual appearance and user experience of the paywall in your iOS or Android app. Let's explore the three available template layout options: :::warning This section describes the legacy Paywall Builder, compatible with Adapty SDK v2.x or earlier. For information on the new Paywall Builder compatible with Adapty SDK v3.x or later, see [New Paywall Builder templates](paywall-builder-templates). ::: ### Overlay The overlay templates offer a versatile and engaging design for your paywalls. It consists of two layers, combining an image at the bottom and a layer showcasing your products or content on top. This approach ensures a visually appealing presentation that captures users' attention. On smaller screens, the layer containing products or content will overlay the image while scrolling, emphasizing the key elements of your paywall. This layered arrangement enhances user interaction and allows you to present multiple offers seamlessly. ### Transparent The transparent layout is ideal for scenarios where you have a concise selection of products or content to present. This layout type offers a captivating visual experience by featuring a full-screen image that immediately grabs users' attention. With no scrolling involved, the image takes up the entire screen space, allowing you to make a bold statement and showcase your offerings in a straightforward manner. Note: Since there is no scrolling in the Transparent Layout, it is best suited for situations where you have a limited amount of content to display. ### Flat The flat layout is reminiscent of a streamlined landing page, presenting all the essential elements in a single continuous layer. Users can scroll through the content seamlessly, experiencing a fluid narrative as they engage with your paywall. This layout type offers a cohesive storytelling approach, enabling you to present your products, services, or content in a compelling and unified manner. Tip: The flat layout is particularly effective when you have a narrative or sequence of offerings to present to your users. ### Device compatibility preview Use the drop-down menu above the asset to select different devices, providing a preview of how your layout will appear on various screens. This feature enables you to ensure that your paywall looks optimal across different devices and screen sizes. ![](https://files.readme.io/a6ccf83-Export-1693241907397.gif) --- # File: paywall-builder-templates.md --- --- title: "Paywall template" description: "Use Adapty’s Paywall Builder templates to create high-converting paywalls." --- Ready-made paywall templates are professionally designed and tailored to streamline your paywall creation process. These templates are crafted by expert designers to help you present your products attractively with minimal effort. Simply add your logo, infuse your brand personality, and you're all set to captivate your audience and drive sales! :::warning This section describes the new Paywall Builder, which works with iOS, Android, and React Native SDKs version 3.0 or higher, and Flutter and Unity SDKs version 3.3.0 or higher. For information on the legacy Paywall Builder compatible with Adapty SDK v2.x or earlier, see [Legacy Paywall Builder templates](paywall-builder-templates-legacy). ::: ## Using Paywall Templates Templates serve as a fantastic starting point, offering beautifully structured layouts and visual appeal. You can either use them as they are or make slight modifications to align them with your brand's aesthetics. Here's why ready-made templates are a smart choice: - **Time-Saving**: Quickly set up a professional-looking paywall without the need for extensive design work. - **Consistency**: Ensure a cohesive look that aligns with proven design standards. - **Customizability**: Personalize each template with your brand elements to make it uniquely yours. For those who prefer a hands-on approach, templates with a minimal design offer a blank canvas. These templates come with basic placements, making it easier for you to unleash your creativity and build a paywall from scratch using Adapty's versatile, feature-rich, and user-friendly Paywall Builder. ## Choose paywall template When creating a new paywall, Adapty offers a selection of templates. You can easily switch between templates at any moment after that. However, it's important to note that replacing a template will discard any changes made to your paywall design. To avoid losing your work, we recommend duplicating the paywall before changing templates so you can return to the saved paywall if needed. 1. Go to the **Layout settings** of the paywall. 2. Click **Change template**. 3. In the opened **Choose paywall** window, browse and select a new template. 4. Click **Choose** to confirm the template change. Please note that replacing a template will discard any changes made to your paywall design. ## Generate paywalls with AI :::info The Paywall AI generator is only available for apps published on the Apple App Store. ::: You can create a unique, high-converting paywall tailored to your app in just seconds using our built-in AI generator. To do so: 1. In the **Layout settings** of the paywall, click **Change template**. 2. CLick **Generate template** on the top of the template selection page. 3. Paste your App Store link and click **Generate paywall**. Generation may take some time. 4. Choose one of the 5 generated templates, or click **Generate new variation** if none of them fit your needs. 5. Click **Pick & Open in Builder**. The selected template will be applied to your paywall. :::note Each user can generate up to 5 sets of templates per day. ::: --- # File: paywall-buttons.md --- --- title: "Paywall button" description: "Customize paywall buttons in Adapty to enhance user interactions and increase conversions." --- :::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: - [iOS](handle-paywall-actions.md) - [Android](android-handle-paywall-actions.md) - [React Native](react-native-handle-paywall-actions.md) - [Flutter](flutter-handle-paywall-actions.md) - [Unity](unity-handle-paywall-actions.md) ::: A paywall button is a UI element that lets users: - Buy products - Sign in - Restore purchases - Close the paywall - Trigger custom actions (e.g., open another paywall) :::info This section describes the new Paywall Builder, which works with: - iOS, Android, and React Native SDKs version 3.0 or higher - Flutter and Unity SDKs version 3.3.0 or higher ::: ### Purchase buttons Purchase buttons: - Connect to selected products in your paywall - Start the purchase when tapped When you add a purchase button to your paywall, it automatically processes purchases your users make. So, you don't need to handle purchases in the app code. :::note You can attract more attention to purchase buttons by animating them. The Paywall builder currently supports **Arrow** and **Pulse** animation types. Note, that, to add the **Arrow** animation, first, you need to configure the **Arrow icon** in the **Content** section. Each animation lets you choose an easing option (Linear, Ease In, Ease Out, Ease In Out) to control how it speeds up or slows down. Animations are available in the Adapty iOS, Android, Flutter, and React Native SDKs starting from version 3.10.0. Follow the [migration guide](migration-to-android-310.md) for Android. ::: ### Links To comply with some store requirements, you can add links to: - Terms of service - Privacy policy - Purchase restoration To add links: 1. Add a **Link** element in the paywall builder. 2. Add the `openUrl` handler to your code: - [iOS](handle-paywall-actions.md) - [Android](android-handle-paywall-actions.md) - [React Native](react-native-handle-paywall-actions.md) - [Flutter](flutter-handle-paywall-actions.md) - [Unity](unity-handle-paywall-actions.md) ### Custom buttons You need custom buttons to: - Close the paywall (`close`) - Open a URL (`openUrl`) - Restore purchases (`restore`) - Sign in (`login`) - Trigger custom actions (e.g., open another paywall) To make most buttons work, you need to **handle their action IDs in your code**: - [iOS](handle-paywall-actions.md) - [Android](android-handle-paywall-actions.md) - [React Native](react-native-handle-paywall-actions.md) - [Flutter](flutter-handle-paywall-actions.md) - [Unity](unity-handle-paywall-actions.md) For example, a close button needs the `close` action handler. :::important `close` is handled automatically in the iOS, Android, and React Native SDKs. `openUrl` is handled automatically in the iOS and Android SDKs. However, if needed, you can override the default behavior. `restore` is always handled automatically. ::: When handling custom actions in your code, you can implement scenarios like: - Opening another paywall - Running multiple actions in sequence (like close and open) Note that you would need to build these scenarios using the action handling system - they're not built-in features. --- # File: paywall-card.md --- --- title: "Paywall card" description: "Design and implement paywall cards in Adapty for better engagement." --- A card is a paywall element that combines several other elements into a single block. The card itself may or may not be visible if this is not needed. To make it visible, add it a background or background picture, frame, etc. :::warning Paywall cards are only available in the [new Paywall Builder](adapty-paywall-builder), which works with iOS, Android, and React Native SDKs version 3.0 or higher, and Flutter and Unity SDKs version 3.3.0 or higher. The legacy Paywall Builder with Adapty SDK v2.x or earlier does not support paywall card functionality. ::: 1. Add a card as a separate element to a paywall or to another paywall element, for example, to a carousel. 2. Add element you need in the card. 3. Configure the card's view: background, shape, frame, etc. --- # File: paywall-carousel.md --- --- title: "Paywall carousel" description: "Set up paywall carousels in Adapty to boost engagement and subscriptions." --- A carousel is a dynamic set of swipeable cards that can be moved left or right, creating a captivating visual experience. It's a fantastic tool to craft paywalls that not only draw attention but also engage users with interactive content. :::warning Carousels are only available in the [new Paywall Builder](adapty-paywall-builder), which works with iOS, Android, and React Native SDKs version 3.0 or higher, and Flutter and Unity SDKs version 3.3.0 or higher. The legacy Paywall Builder with Adapty SDK v2.x or earlier does not support paywall carousel functionality. ::: Use the power of carousels to elevate your paywall's appeal: 1. **Add Cards**: Start by adding cards to your carousel, each one a canvas for your creative touch. 2. **Customize Cards**: Populate your cards with various elements—images, text, buttons, and more. The carousel's height setting ensures all cards are uniformly sized, delivering a sleek and consistent appearance. Embrace the flexibility of carousels to showcase featured products, highlight exclusive offers, or narrate compelling stories. With this engaging element, your paywalls will not only stand out but also provide a seamless and immersive user experience. --- # File: paywall-dark-mode.md --- --- title: "Paywall dark mode" description: "Enable dark mode for paywalls in Adapty to improve user experience." --- Dark mode has become an essential feature for improving user experience in mobile apps, especially for users who spend long periods interacting with content. Most apps that support dark mode also choose to have two different versions of the paywall (light and dark) for consistency, and Adapty allows doing exactly that. When dark mode is enabled, the paywall automatically matches the device’s current mode: it shows the light version if the device is in light mode, and the dark one if it’s in dark mode. :::note To use the Paywall dark mode, you’ll need a paid plan: Pro, Pro+, or Enterprise. ::: :::warning Dark mode is supported: - iOS: starting with v3.1.0 - Android: starting with v3.1.1 - Flutter: starting with v3.2.0 - React Native: starting with v3.1.0 - Unity: starting with v3.3.0 It’s also available in fallback paywalls. ::: To set up dark mode for your paywall: 1. First, enable dark mode in the paywall’s **Layout settings**: 2. Now, you can configure light and dark modes separately. To switch to dark mode, turn on the **Dark Mode** toggle in the left paywall menu: 3. Once you’ve switched to dark mode, you can adjust the elements as needed. Dark mode lets you use a different image or video, as well as separate color and background options. --- # File: paywall-device-compatibility-preview.md --- --- title: "Paywall device compatibility preview" description: "Preview paywall compatibility across devices for an optimized experience." --- :::warning This section describes the new Paywall Builder, which works with iOS, Android, and React Native SDKs version 3.0 or higher, and Flutter and Unity SDKs version 3.3.0 or higher. For information on the legacy Paywall Builder compatible with Adapty SDK v2.x or earlier, see [Legacy Paywall Builder device compatibility preview](paywall-layout-and-products-legacy#device-compatibility-preview). ::: You can preview your paywall on different screen types using the panel on the right side of the paywall builder. This helps ensure your paywall looks great across various devices and screen sizes. From here, you can: - Select the device to preview your paywall on. - Switch between horizontal and vertical preview modes — especially useful for paywalls designed for iPad. - Zoom in or out of the preview. - Preview [tags variables for product info](https://adapty.io/docs/paywall-builder-tag-variables#how-to-use-tag-variables). :::tip Set the [maximum width](https://adapty.io/docs/paywall-layout-and-products#content-layout) of elements to optimize layout on iPads. ::: --- # File: paywall-head-picture.md --- --- title: "Paywall hero image" description: "Customize your paywall with a head picture to improve conversion rates in Adapty." --- The hero image is the star of your paywall, setting the tone, establishing the theme, and capturing users' attention right from the start. This image plays a crucial role in shaping the look and feel of your paywall on both iOS and Android platforms. :::warning This section describes the new Paywall Builder, which works with iOS, Android, and React Native SDKs version 3.0 or higher, and Flutter and Unity SDKs version 3.3.0 or higher. For information on the legacy Paywall Builder compatible with Adapty SDK v2.x or earlier, see [Legacy Paywall Builder head picture](paywall-layout-and-products-legacy#main-image-and-sizing). ::: ## Hero image format and size Your main image is the centerpiece of your paywall's design, essential for captivating users and driving them to take action. Follow these guidelines to ensure your hero image is effective and visually appealing: - **Formats**: JPG and PNG. - **Recommended Size**: Files up to 2 MB for faster loading. - **Image Composition**: Photos with the main object centered and surrounded by ample space usually communicate your message effectively. - **Impactful Visuals**: Emotional or vibrant photos work well. - **Graphics Use**: Ideal for adding visual appeal, with separate spaces reserved for text. You have control over the sizing of the main image, adjusting its proportions to achieve the desired visual balance on your paywall. Specify the image size as a percentage of the total screen area for perfect alignment. ## Hero picture layout options The **overlay hero image** adds a layer of depth and dynamism to your paywall. Positioned as a fixed background at the bottom, it creates a stunning effect as other elements scroll over it. This makes the hero image appear stationary, providing a visually engaging experience as users scroll through the content. The **transparent layout** delivers a bold, full-screen hero image that instantly captures attention. This layout is perfect for showcasing a limited selection of products or content, filling the entire screen and making a powerful, direct impact without the need for scrolling. :::note Use the transparent layout for minimal content display, as it doesn’t involve scrolling, making your message clear and impactful. ::: The **flat layout** mimics a seamless landing page, presenting all elements in a continuous, scrollable layer. Users enjoy a smooth, cohesive narrative as they scroll through the content, perfect for integrating your products or stories effectively in a unified flow. :::note Ideal for storytelling or presenting a series of offerings, the flat layout lets you create a compelling sequence that captivates users. ::: ## Hero image mask The **mask type** defines the shape of the main image, allowing you to apply creative effects that enhance the visual presentation. For flat or overlay image layouts, choose from various mask types to suit your design. Adjust the roundness of the image mask using numerical values to achieve the perfect look for your hero image. ## How to remove a hero image To remove a hero image from a paywall: 1. Open the **Hero image** element. 2. Change its height to zero. --- # File: paywall-layout-and-products-legacy.md --- --- title: "Paywall layout and products" description: "Manage paywall layouts and products in Adapty’s legacy system." --- [Adapty Paywall Builder](adapty-paywall-builder-legacy) simplifies the process of creating paywalls—specialized screens within your app where users can make purchases. This tool eliminates the need for technical expertise or design skills. You can effortlessly customize how your paywalls look, the messages they convey, and where essential buttons are positioned. What's more, you can even make real-time changes to these screens while your app is running — without App Store/Google Play reviews. Moreover, Adapty empowers you to optimize your paywalls further with [A/B testing](ab-tests). Alongside the Paywall Builder, this allows you to test different variations of your paywalls to find the most effective design and messaging. Whether you're striving to increase sales, promote content, or grant access to exclusive features, the Paywall Builder provides a user-friendly solution to accomplish these objectives. In this section, we will discuss the customization of the layout and products of your paywalls. :::warning This section describes the legacy Paywall Builder, compatible with Adapty SDK v2.x or earlier. For information on the new Paywall Builder compatible with Adapty SDK v3.x or later, see [Paywall layout in new Paywall Builder](paywall-layout-and-products). ::: ### Layout After selecting the preferred layout type and corresponding template for your paywall in Adapty's Paywall Builder, you gain the ability to shape the visual appearance of your paywall, making it engaging and aligned with your brand's aesthetics. This tab offers a range of controls that allow you to customize various aspects of the paywall's layout, background, and appearance. Let's explore the controls and options available in the Layout tab: #### Main image and sizing The **main image** is the centerpiece of your paywall's design, influencing the overall look and feel. This image serves to captivate users and encourage them to take action. Here are some guidelines to consider when selecting and uploading your main image: - A photo should be PNG or JPEG \< 2Mb. - Photos with the main object in the center and some free space around it usually make the message clear. - Emotional and/or bright photos work. - Graphic works but use it without claims as there is a separate place for texts in the asset. You have the ability to control the sizing of the main image, determining its proportions in relation to the entire paywall screen. This helps in achieving the desired visual balance and impact. Specify the image sizing as a percentage of the total screen area. #### Mask type for the image The mask type determines the shape of the main image, allowing you to apply creative effects to the visual presentation. Choose from the following mask types: - Rectangle - Rounded Rectangle - Circle - Leaf You can adjust the roundness of the image mask using numerical values (not available for circle type). #### Background color The background color of the paywall sets the tone for the entire experience. You can choose a background color that aligns with your brand's identity or complements the image. The background color acts as a canvas that enhances the visual appeal of the paywall. You have the option to select either a solid color or a gradient color for the background. #### Font settings of your paywall It's important to keep your paywall visually consistent with the rest of your app — and one of the biggest visual factors is the font that you're using. You can choose to simply have a system font for your paywall (SF Pro for iOS, Roboto for Android), use one of the available common fonts or upload your own custom font: :::note Changing the font settings in Layout affects all the other labels on the paywall (unless they have been customised before that). You can learn how to upload your custom font [here](using-custom-fonts-in-paywall-builder). ::: #### Hard and soft paywalls A key decision you can make in the layout tab is whether to enable a closing button, resulting in a soft paywall, or to remove it, resulting in a hard paywall. By toggling the **Hard paywall **option, you can instantly see how the closing button adapts or disappears, based on your choice. For soft paywalls, you can set up the view and behavior of the closing paywall button. #### Close button, its style, placement and fade-in animation The presence of the closing icon provides users with the means to dismiss the paywall and continue their interaction with the app. For soft paywalls, you can define the view of the closing paywall button and how fast it will appear: 1. Switch off the **Hard paywall** toggle. 2. In the expanded section, pick how the button should look and where it should be. The preview on the right will instantly change to reflect your choice. - **Close icon type:** Choose the icon for the Close button or **Custom text** for text buttons. - **Icon placement:** Position the Close button either at the top-left, center or top-right part of the screen. - **Color and opacity:** You can control the color and opacity of both the content and the background of the closing button. You can make the closing button fit your paywall better by adjusting the colors and/or removing the background of a button entirely. 3. To add a delay before showing this Close button, switch on the **Show after delay** toggle. 4. In the expanded section, specify the delay duration (in milliseconds) before the Close button starts fading in and indicate how long the button's fade-in animation should last. Here is a video to show the whole process: #### Device compatibility preview Use the drop-down menu above the asset to select different devices, providing a preview of how your layout will appear on various screens. This feature enables you to ensure that your paywall looks optimal across different devices and screen sizes. ### Products In the products tab of Adapty's Paywall Builder, you have the ability to create and customize a visually appealing product section that showcases your offerings to users. This tab enables you to configure various aspects of the product's appearance and textual content. Let's delve into the options available for customization in the products tab: #### 1. Product section layout You can define how your product section is presented to users by choosing between a Horizontal List or a Vertical List layout. This layout selection influences how your products are arranged on the screen. #### 2. Main product Highlighting a specific product can draw user attention. In the main product configuration, you can select the product that will be emphasized and preselected in the product section by a special border. This can be particularly useful for promoting a featured item or a special offer. You can also add badge text to this product to provide additional context or highlight its uniqueness — see how below. #### 3. Products customization Let's take a look at how you can customize each product on your paywall: (1, 2) You can adjust the color and the corner radius of the product containers for a unique visual touch. And you can also have a badge for every product on a paywall to provide additional context such as the savings for the user or highlight its uniqueness. (3) You can also control the font for each of the text labels — bold, italic or regular as well as upload your own custom fonts (learn more about it [here](using-custom-fonts-in-paywall-builder)): (4) But what's probably more important is that you can describe your products in text and fine-tune text attributes such as size, color and style. You can also use optional second title and subtitle to provide more details — for example, to let people compare the pricing between products more easily. This level of control allows you to craft a visually appealing and informative product display within your paywall, optimizing the user experience. :::note Using tag variables Every text field in the product block supports [tag variables](paywall-builder-tag-variables) such as `` for the title of the product. We strongly recommend using them for easier localization. ::: (5) If you intend to use this paywall in cases where a user might be eligible for an offer, you should configure the offer text for every expected type. And of course you can adjust its color and size as well. If a user is found eligible — the corresponding offer text will be shown as a subtitle on the product card. :::warning Default text for offers **Note**: eligibility for an offer is determined on a device by SDK. In case you haven't provided a custom text for it and the user was found eligible — our SDK will show a default text corresponding to the offer type. Learn more about offers [here](offers). ::: #### Product styles: synced or separate By default changing each of the style components above (such as any color, font size, or corner radius) applies to all of the products on a paywall. But if you want to make a particular product pop, you can disable the Style sync for this product and tune its visuals separately. This can be especially useful when highlighting the main product. You can disable Style sync for a product in the upper right corner. After that, any changes you make to the visuals will only be applied to this product: You can use this control to style any one of your products separately from the rest. But remember: when you turn style sync back on after making changes — this will revert them, so be careful. Style sync also doesn't affect texts as those are always separate for every product. #### Preview products on your Paywall Once you've finished customizing how your products look, it makes sense to double-check the result using preview before testing it on a device. You can find some useful settings for it in the "eye" icon in the top-right corner: There you can toggle whether you'd like to see our placeholders for the tag variables or the actual values of the tags. Or you can simulate what the paywall will look like if a user is found eligible for a certain offer type. :::warning Preview uses placeholder values for tag variables **Remember**: if you use tag variables for your products, the Preview only shows some hardcoded placeholder values for them. This is applied both to prices and titles for both the products and offers. The actual values will be retrieved from the App Store and Google Play and shown only on the device. ::: --- # File: paywall-layout-and-products.md --- --- title: "Paywall layout" description: "Design paywall layouts and manage products in Adapty for better conversion." --- After selecting a template for your paywall in Adapty's Paywall Builder, you can customize the paywall's visual appearance to match your brand's style. The Layout settings provide a variety of controls for adjusting the layout, background, and overall look of the paywall. Let's explore these settings: The layout settings control the basic aspects of the paywall, including the template, background color, default fonts, purchase flow, content layout, and top buttons. :::warning This section describes the new Paywall Builder, which works with iOS, Android, and React Native SDKs version 3.0 or higher, and Flutter and Unity SDKs version 3.3.0 or higher. For information on the legacy Paywall Builder compatible with Adapty SDK v2.x or earlier, see [Legacy Paywall Builder paywall layout](paywall-layout-and-products-legacy#layout). ::: ## Purchase flow Decide how users will complete purchases. There are two options: - **Products as list + purchase button**: Users select products first, then click the buy button to start the purchase. - **Products as purchase buttons**: Each product is a button, and the purchase begins when the user clicks a product button. ## Background color Maintain visual consistency by setting a default background for your paywall. Use the **Background color** field in the **Layout settings**. Click the colored square to open the configuration window, where you can choose a solid color or a gradient in separate tabs. ## Font settings of your paywall It's important to keep your paywall visually consistent with the rest of your app — and one of the biggest visual factors is the font that you're using. You can choose to simply have a system font for your paywall (SF Pro for iOS, Roboto for Android), use one of the available common fonts, or upload your own custom font: Font settings in the **Layout settings** apply to all paywall components by default. You can override these settings for specific elements, such as text boxes or lists, when editing those elements individually. If you change the default font in the **Layout settings**, it will not affect elements with individual fonts. Learn how to upload a custom font [here](using-custom-fonts-in-paywall-builder). ## Content layout You don't have to manually fine-tune margins and width for each content element of the paywall. Go to the **Content layout** to adjust all the following settings for all content elements at once: - **Default child margin**: Defines space around each child element. - **Spacing**: Defines space between elements inside a layout. - **Max width**: Sets the maximum width of elements to optimize layout on iPads. We recommend 600pt for a clean, balanced layout. :::warning Max width parameter is only available starting with Adapty SDK v3.7.0 and higher. ::: To adjust the layout for a specific element—such as setting the maximum width for the footer—go to the **Layout** section under **App Icon, Header, Feature List, Products**, or **Footer**. ## Top buttons Add up to 2 top buttons to your paywall to provide users with options like closing the paywall. Customize their appearance and behavior as follows: 1. Enable the **Top Button** or **Top Button 2** toggle. 2. Choose the button's look and position. The preview will update instantly. | Button setting | Description | |--------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Action |

Choose the action that the paywall should perform when a user clicks this button. If you choose standard actions, the paywall will generate a standard event you will be able to handle in a standard way in your mobile app code.

If you choose a custom action, you will need to process the action by its `CustomActionID` in your mobile app code.

| | Style | Choose if you want the button to look like an icon or have a text. If you choose an icon, choose the icon type in the | 3. To delay the appearance of the button, adjust the **Show after delay** slider. --- # File: paywall-localization.md --- --- title: "Paywall localization" description: "Localize paywalls in Adapty to reach global audiences." --- In a world with many cultures, it's important to adapt your product for each country. You can do this by using paywall localizations. For each paywall, you can make versions in different languages to match the needs of specific local markets. Depending on what you use to design your paywalls, adding locale varies: 1. [Adding paywall locale in Adapty Paywall Builder](add-paywall-locale-in-adapty-paywall-builder) 2. [Adding paywall locale in remote config](add-remote-config-locale) Once you add locales to a paywall, learn how to [correctly work with locale codes in your app's code](localizations-and-locale-codes). --- # File: paywall-metrics.md --- --- title: "Paywall metrics" description: "Track and analyze paywall performance metrics to improve subscription revenue." --- Adapty collects a series of metrics to help you better measure the performance of the paywalls. All metrics are updated in real-time, except for the views, which are updated once every several minutes. All metrics, except for the views, are attributed to the product within the paywall. This document outlines the metrics available, their definitions, and how they are calculated. Paywall metrics are available on the paywall list, providing you with an overview of the performance of all your paywalls. This consolidated view presents aggregated metrics for each paywall, allowing you to assess their effectiveness and identify areas for improvement. For a more granular analysis of each paywall, you can navigate to the paywall detail metrics. In this section, you will find comprehensive metrics specific to the selected paywall, offering deeper insights into its performance. ### Metrics controls The system displays the metrics based on the selected time period and organizes them according to the left-side column parameter with three levels of indentation. For Live paywall, the metrics cover the period from the paywall's start date until the current date. For inactive paywalls, the metrics encompass the entire period from the start date to the end of the selected time period. Draft and archived paywalls are included in the metrics table, but if there is no data available for those paywalls, they will be listed without any displayed metrics. #### View options for metrics data The paywall page offers two view options for metrics data: placement-based and audience-based. In the placement-based view, metrics are grouped by placements associated with the paywall. This allows users to analyze metrics by different placements. In the audience-based view, metrics are grouped by the target audience of the paywall. Users can assess metrics specific to different audience segments. You can select the preferred view using the dropdown option at the top of the paywall detail page. #### Profile install date filtration The Filter metrics by install date checkbox enables the filtering of metrics based on the profile install date, instead of the default filters that use trial/purchase date for transactions or view date for paywall views. By selecting this checkbox, you can focus on measuring user acquisition performance for a specific period by aligning metrics with the profile install date. This option is useful for customizing the metrics analysis according to your specific needs. #### Time ranges You can choose from a range of time periods to analyze metrics data, allowing you to focus on specific durations such as days, weeks, months, or custom date ranges. #### Available filters and grouping Adapty offers powerful tools for filtering and customizing metrics analysis to suit your needs. With Adapty's metrics page, you have access to various time ranges, grouping options, and filtering possibilities. - Filter by: Audience, country, paywall, paywall state, paywall group, placement, country, store, product, and product store. - Group by: Product and store. You can find more information about the available controls, filters, grouping options, and how to use them for paywall analytics in [this documentation.](controls-filters-grouping-compare-proceeds) #### Single metrics chart One of the key components of the paywall metrics page is the chart section, which visually represents the selected metrics and facilitates easy analysis. The chart section on the paywall metrics page includes a horizontal bar chart that visually represents the chosen metric values. Each bar in the chart corresponds to a metric value and is proportional in size, making it easy to understand the data at a glance. The horizontal line indicates the timeframe being analyzed, and the vertical column displays the numeric values of the metrics. The total value of all the metric values is displayed next to the chart. Additionally, clicking on the arrow icon in the top right corner of the chart section expands the view, displaying the selected metrics on the full line of the chart. #### Total metrics summary Next to the single metrics chart, the total metrics summary section is shown, which displays the cumulative values for the selected metrics at a specific point in time, with the ability for you to change the displayed metric using a dropdown menu. ### Metrics definitions #### Revenue This metric represents the total amount of money generated in USD from purchases and renewals. Please note that the revenue calculation does not include the App Store / Play Store commission and is calculated before deducting any fees. #### Proceeds This metric represents the actual amount of money received by the app owner in USD from purchases and renewals after deducting the applicable App Store / Play Store commission. It reflects the net revenue that directly contributes to the app's earnings. For more information on how proceeds are calculated, you can refer to the Adapty [documentation.](analytics-cohorts#revenue-vs-proceeds) #### ARPPU ARPPU is an average revenue per paying user. It’s calculated as total revenue divided by the number of unique paying users. $15000 revenue / 1000 paying users = $15 ARPPU. #### ARPAS The average revenue per active subscriber allows you to measure the average revenue generated per active subscriber. It is calculated by dividing the total revenue by the number of subscribers who have activated a trial or subscription. For example, if the total revenue is $5,000 and there are 1,000 subscribers, the ARPAS would be $5. This metric helps assess the average monetization potential per subscriber. #### Unique conversion rate (CR) to purchases The unique conversion rate to purchases is calculated by dividing the number of purchases by the number of unique views. For example, if there are 10 purchases and 100 unique views, the unique conversion rate to purchases would be 10%. This metric focuses on the ratio of purchases to the unique number of views, providing insights into the effectiveness of converting unique visitors into paying customers. #### CR to purchases The conversion rate to purchases is calculated by dividing the number of purchases by the total number of views. For example, if there are 10 purchases and 100 views, the conversion rate to purchases would be 10%. This metric indicates the percentage of views that result in purchases, providing insights into the effectiveness of your paywall in converting users into paying customers. #### Unique CR to trials The unique conversion rate to trials is calculated by dividing the number of trials started by the number of unique views. For example, if there are 30 trials started and 100 unique views, the unique conversion rate to trials would be 30%. This metric measures the percentage of unique views that result in trial activations, providing insights into the effectiveness of your paywall in converting unique visitors into trial users. #### Purchases Purchases represent the cumulative total of various transactions made on the paywall. The following transactions are included in this metric (renewals are not included): - New purchases, that are made directly on the paywall. - Trial conversions of trials that were initially activated on the paywall. - Downgrades, upgrades, and cross-grades of subscriptions made on the paywall. - Subscription restores on the paywall, such as when a subscription is reinstated after expiration without auto-renewal. By considering these different types of transactions, the purchases metric provides a comprehensive view of the overall acquisition and monetization activity on your paywall. #### Trials The trials metric represents the total number of trials that have been activated. It reflects the number of users who have initiated trial periods through your paywall. This metric helps track the effectiveness of your trial offering and can provide insights into user engagement and conversion from trials to paid subscriptions. #### Trials canceled The trials canceled metric represents the number of trials in which the auto-renewal feature has been switched off. This occurs when users manually unsubscribe from the trial, indicating their decision not to continue with the subscription after the trial period ends. Tracking trials canceled provides valuable information about user behavior and allows you to understand the rate at which users opt out of the trial. #### Refunds The refunds metric represents the number of refunded purchases and subscriptions. This includes transactions that have been reversed or refunded due to various reasons, such as customer requests, payment issues, or any other applicable refund policies. #### Refund rate The refund rate is calculated by dividing the number of refunds by the number of first-time purchases (renewals are not included). For example, if there are 5 refunds and 1000 first-time purchases, the refund rate would be 0.5%. #### Views The views metric represents the total number of times the paywall has been viewed by users. Each time a user visits the paywall, it is counted as a separate view. For example, if a user visits the paywall two times, it will be recorded as two views. Tracking views helps you understand the level of engagement and user interaction with your paywall, providing insights into user behavior and the effectiveness of your paywall placement and design. #### Unique views The unique views metric represents the number of unique instances in which the paywall has been viewed by users. Unlike total views, which count each visit as a separate view, unique views count each user's visit to the paywall only once, regardless of how many times they access it. For example, if a user visits the paywall two times, it will be recorded as one unique view. Tracking unique views helps provide a more accurate measure of user engagement and the reach of your paywall, as it focuses on individual users rather than the total number of visits. :::warning Make sure to send paywall views to Adapty using `.logShowPaywall()` method. Otherwise, paywall views will not be accounted for in the metrics and conversions will be irrelevant. ::: --- # File: paywall-product-block.md --- --- title: "Paywall product list" description: "Discover how to configure paywall product blocks in Adapty to optimize in-app purchases." --- The product list is a key element of your paywall that showcases your offerings in an organized, attractive manner. This list is crucial for guiding users toward making a purchase. The **Content** tab contains the products that will be displayed in the paywall. These are the same products you added to the paywall when you created it. You can adjust the list of products. This will affect the product list in the **General** tab of the paywall. After you review the list of products: 1. Choose which product should be preselected by default in the **Selected product** field. 2. Define how a product should look if it is selected or not in the **Style** tab of the **Products** section. 3. Configure the overall view of the block in the **Layout** tab or [add products groups](#add-products-group) to combine layouts. :::warning This section describes the new Paywall Builder, which works with iOS, Android, and React Native SDKs version 3.0 or higher, and Flutter and Unity SDKs version 3.3.0 or higher. For information on the legacy Paywall Builder compatible with Adapty SDK v2.x or earlier, see [Legacy Paywall Builder paywall products](paywall-layout-and-products-legacy#products). ::: ## Product view customisation Enhancing the visual appeal of specific products can significantly rebalance user attention. Highlighting a product or special offer can encourage users to focus on it. Let’s look at some powerful customization options. ### Product badge A product badge is a small label that can be added to a product. This badge can provide additional promotional information and direct user choice. The badge size automatically adjusts to fit the text, and its position is optimized for your paywall layout. To add a product badge: 1. Turn on the **Product badge** toggle in the setting of a specific product. 2. Customize the badge view and text to suit your design needs. ### Selected product For the **Products as list + purchase button** purchase flow, you can **preselect** a product to gently nudge users towards it. This can be especially effective in directing user choice. If you add several [product groups](#add-products-group), the same **Selected product** option will be applied to all groups. To preselect a product: 1. Open the **Products** element. 2. In the **Content** tab, choose the product you want to preselect from the **Selected product** drop-down list. 3. Adjust the view of the selected product as well as the default view of other products in the **Style** tab if necessary. ### Highlighted product For the **Products as purchase buttons** purchase flow, you can **highlight** a preferred product to make it the primary choice, drawing immediate user attention. To highlight a product: 1. In the left pane, choose the product you want to highlight. 2. In the **Style** subsection, adjust the design to make the product stand out more. ### Product offers Each product can feature different text for offers in the **Text** subsection. The **Default** tab contains the text displayed without an offer. This is a good place to use: - [tag variables](paywall-builder-tag-variables) for dynamic, localized content - [custom tags](custom-tags-in-paywall-builder) for your own dynamic content Start typing with a triangle bracket, and Adapty will suggest available tag variables to insert localized data from the stores. ## Switch between 2 product sets by trial toggle To create a versatile user experience, you can allow users to switch between two sets of products using a toggle. This is especially useful for differentiating between standard products and trials. To add a toggle: 1. In the **Products** element, change the **Products grouping** option to **Toggle (for free trials and other offers)**. This will add two subsections: **Toggle on** and **Toggle off**. 2. Add products to both subsections to create views for when the toggle is on or off. 3. In the **Toggle** element, set the **Default state** to choose whether the toggle should start as on or off in the paywall. ## Switch between product sets by tab Paywall tabs let you group your products into categories, highlighting all possible options for your users. They're especially helpful if: - Your app offers multiple weekly, monthly, or yearly plans - You have different tiers like Plus, Gold, or Premium You can also add elements like feature lists in tabs to help users see the differences between tiers. To add tabs: 1. In the **Products** element, set **Products grouping** to **Tabs (for comparing plan groups)**. This will split your products into two initial tab groups. 2. If you need more tabs, click **Add tab group**. 3. Organize your products within these tabs. 4. Open the first tab group, and in the **Tab title**, enter the name that will appear on the paywall. 5. Give an internal tab name in a separate field for easy reference. This name won’t be visible on the paywall but can help you identify the tab in lists. 6. Repeat steps 4-5 for each tab. 7. Choose which tab will be active when the user opens the paywall. Go to **Tab control** and select the default tab from the **Selected tab** list. ## Show extra products under a button To keep your paywall simple, you can hide some products or product groups under a button (like "View more plans" or any label you prefer). This helps users focus on your top options first while still allowing them to explore other plans if they want. It's a great way to make the paywall cleaner and improve conversions. Here’s how: 1. In the **Products** element, set the **Products grouping** option to **Button (for more alternative plans)**. This will split your products into two groups: **Shown** and **More plans**. 2. Distribute your products between these groups. **Shown** is for products you want displayed immediately. **More plans** is for products hidden behind the button, shown only when users click it. 3. Customize the text and layout of the button in the **View more plans** element to suit your needs. These options help you build a clear, visually appealing product list that guides users toward purchase. ## Show extra plans in a bottom sheet To simplify your paywall, you can hide some products and display them only when users click a button (like "View More Plans" or any label you choose). This action opens a sliding bottom sheet with the hidden products. This setup helps users focus on your main options first while still giving them the flexibility to explore additional plans if they're interested. It's an effective way to declutter the paywall and potentially boost conversions. Here’s how: 1. In the **Products** element, set the **Products grouping** option to **Bottom Sheet (for more alternative plans)**. This will split your products into two groups: **Shown** and **More plans**. 2. Distribute your products between these groups. **Shown** is for products you want displayed immediately. **More plans** is for products that are initially hidden and shown only when users click the button. 3. Customize the text and layout of the button in the **View More Plans** element to fit your design and messaging. 4. The bottom sheet will automatically use the same product list display format as your main paywall, whether products are separate purchase buttons or each product acts as a button. You can customize the bottom sheet layout, text, style, and default product selection. These options help you create a simple, user-friendly product list. ## Add products group If you want to apply both vertical and horizontal layouts to different products or add text between products, you can add another products group. :::note Adding a products group disables the [Products grouping](#switch-between-2-product-sets-by-trial-toggle) option. Choose between adding another products group or grouping products inside the same block. ::: To add a products group: 1. Click **Add element** or **+** on the **Footer**. 2. Select **Products**. 3. Add products. Since you can't have the same product in different groups, you need to first delete it from another group. --- # File: paywall-texts-and-buttons.md --- --- title: "Paywall texts and buttons" description: "Customize paywall texts and buttons to improve conversion rates." --- [Adapty Paywall Builder](adapty-paywall-builder-legacy) simplifies the process of creating paywalls—specialized screens within your app where users can make purchases. This tool eliminates the need for technical expertise or design skills. You can effortlessly customize how your paywalls look, the messages they convey, and where essential buttons are positioned. What's more, you can even make real-time changes to these screens while your app is running — without App Store/Google Play reviews. Moreover, Adapty empowers you to optimize your paywalls further with [A/B testing](ab-tests). Alongside the Paywall Builder, this allows you to test different variations of your paywalls to find the most effective design and messaging. Whether you're striving to increase sales, promote content, or grant access to exclusive features, the Paywall Builder provides a user-friendly solution to accomplish these objectives. :::warning This section describes the legacy Paywall Builder, compatible with Adapty SDK v2.x or earlier. For information on the new Paywall Builder compatible with Adapty SDK v3.x or later, see [Paywall buttons in new Paywall Builder](paywall-buttons). ::: In this section, we will discuss the customization of buttons and text elements within your paywalls. ### Buttons In the buttons tab, you have the ability to define and customize various buttons that play a crucial role in guiding user interactions and enhancing the overall user experience of your paywall. This tab empowers you to configure primary Call to action (CTA) buttons as well as secondary buttons that can direct users to essential legal information. You can enable or disable the appearance of the secondary buttons. Let's deep dive into the options available for customization in the buttons tab: #### Primary call to action button The primary call to action button serves as the key action that you want users to take. You can tailor its appearance and text to align with your paywall's goals: - Button text: Define the text that appears on the primary call to action button. - Text font size: Adjust the font size of the button text to ensure optimal readability. - Button color: Choose a color that stands out and draws users' attention. - Button roundness: Modify the roundness of the button's corners for a unique aesthetic. - Button text color: Select a text color that complements the button's background and enhances legibility. #### Secondary buttons In addition to the primary call to action button, you can include secondary buttons that direct users to essential legal information. These buttons typically appear at the bottom of the paywall and provide users with access to important resources such as terms and privacy of the app usage. You can configure each secondary button with the following settings such as text, text size, text color, and URLs. Apart from the traditional button configurations, the Restore and Login buttons provide specific functionalities: - Restore: This button is used to allow users to restore their previous purchases or access content they've previously owned. - Login: The login button facilitates user authentication and access to personalized content. ### Texts In the texts tab, you can make your paywall sound attractive and clear. This tab helps you tell users why your paywall is awesome. Just choose your words, make them look nice, and guide users through the cool stuff they'll get. Feel free to use [custom tags](https://dash.readme.com/go/adaptyteam?redirect=%2Fv2.0%2Fdocs%2Fcustom-tags-in-paywall-builder) to personalize your UI text and [custom fonts](paywall-builder-tag-variables) to make your paywall blend in more with the rest of your app's design. Here are the main elements of the tab: #### Headline and subhead Make a catchy title and a small introduction that sets the mood. Keep it short and interesting. #### Main features Under the headline and subhead, show what cool stuff your paywall offers. You can do this in two ways: - Feature list: Make a list of cool things users get with your subscription. You can add icons to show what each feature is about. - Timeline: Show how things get better over time. Give each step a title, a small description, and an icon. For all of these elements you can control the alignment with the page, text size, and color individually. --- # File: paywall-timer.md --- --- title: "Paywall timer" description: "Use Adapty’s paywall timer feature to increase conversions and create urgency." --- 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. :::warning Paywall timers are only available in the [new Paywall Builder](adapty-paywall-builder), which works with iOS, Android, and React Native SDKs version 3.0 or higher, and Flutter and Unity SDKs version 3.3.0 or higher. The legacy Paywall Builder with Adapty SDK v2.x or earlier does not support paywall timer functionality. ::: You can customize the text before and after the timer to create the desired message, such as: "Offer ends in: 10:00 sec." 1. Add a timer as a separate element to a paywall or to another paywall element, like a card. 2. Configure the timer's settings: format and separator, start value, text before and after (if needed), color, font, spacing, etc. ## Timer mode You can control how the timer behaves when users see it by using the **Timer mode** parameter. 3 standard modes work out of the box—just select the required option from the dropdown list: | Mode | Description | | ------------------------------------- | ------------------------------------------------------------ | | **Reset timer on every paywall view** | The timer resets every time the user sees the paywall, starting from the initial value each time. | | **Reset timer on every app launch** | The timer starts the first time the user sees the paywall and keeps counting in the foreground or background until the app is restarted. If the user sees the paywall multiple times in the same session, they’ll see the same timer counting down. Once the app is closed, the timer resets, and the next time the app is opened, the timer restarts from the beginning. | | **Keep timer across app launches** | The timer starts the first time the user sees the paywall and keeps counting in the foreground or background, even if the app is closed. The user will see the same timer every time they return to the paywall, regardless of app or paywall restarts. | | **Developer defined** | You can set up any timer you need directly in your mobile app code. Start by entering a **Timer ID**, then use it in your code as explained in the [How to set up developer-defined timers in your mobile app](paywall-timer#how-to-set-up-developer-defined-timers-in-your-mobile-app) section to configure the timer however you like. | ## What happens when the timer ends? You can customize what happens when the timer runs out. Should it display another screen with a new opportunity? Or maybe show a different paywall? It requires some coding, but with our docs, you'll handle it. 1. Turn on the **Trigger custom action when the timer runs out** toggle. 2. Enter the ID of the action you want to trigger in the **Timer action ID** field. 3. Use this action ID in your app to define what should happen when the timer ends. Treat it like any other custom action, as explained in our **Handling Events: Actions** guide for [iOS](ios-handling-events#actions) and [Android](android-handling-events#actions). ## How to set up developer-defined timers in your mobile app? 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. ```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 */ } ) } ``` 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). ```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). ```dart showLineNumbers try { final view = await AdaptyUI().createPaywallView( paywall: paywall, customTags: ..., customTimers: { 'CUSTOM_TIMER_6H': DateTime.now().add(const Duration(seconds: 3600 * 6)), 'CUSTOM_TIMER_NY': DateTime(2025, 1, 1), // New Year 2025 }, preloadProducts: ..., ); } 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. ```csharp showLineNumbers var parameters = new AdaptyUICreateViewParameters() .SetCustomTimers( new Dictionary { { "CUSTOM_TIMER_6H", DateTime.Now.AddHours(6) }, // 6 hours { "CUSTOM_TIMER_NY", new DateTime(2025, 1, 1) } // New Year 2025 } ) AdaptyUI.CreateView(paywall, parameters, (view, error) => { // handle the result }); ``` - 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. ```typescript showLineNumbers let customTimers = { 'CUSTOM_TIMER_NY': new Date(2025, 0, 1) } //and then you can pass it to createPaywallView as follows: 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 `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). --- # File: paywall-video.md --- --- title: "Paywall hero video" description: "Enhance paywalls with video content to boost engagement in Adapty." --- Adding a video clip to your paywall is a great way to engage users and increase conversions. Videos can explain your offers more clearly, highlight key features, and create a more dynamic, visually appealing experience that grabs attention. :::note To use the Paywall hero video, you’ll need a paid plan: Pro, Pro+, or Enterprise. ::: :::warning Hero video is supported on Adapty SDK: - iOS: starting with v3.1.0 - Android: starting with v3.1.1 - Flutter: starting with v3.2.0 - React Native: starting with v3.1.0 - Unity: starting with v3.3.0 If the video isn't supported or in fallback cases, the first frame of the video will be shown instead. ::: Add the **Hero video** in place of the **Hero image** element: 1. First, switch to the video mode. 2. Then, drag and drop your video file into the **Video file** area. ## Supported formats | Specification | Details | |----------------|---------------| | Extensions | MP4 and WEBM | | Minimum size | 640х480 | | Maximum length | 30 sec | | Audio | Not supported | --- # File: paywalls.md --- --- title: "Paywalls" description: "Explore Adapty’s paywall system and best practices for revenue growth." --- A paywall serves as an in-app storefront where customers can browse and make purchases. What sets it apart is its dynamic nature, allowing you to modify it without requiring app updates. You can even introduce new paywalls to users based on various factors using [Placements](placements). There are two ways to design a paywall: - **[Paywall Builder](adapty-paywall-builder)**: An easy and flexible no-code tool designed to help you create top-performing paywalls without requiring development or design skills. - **[Remote Config](customize-paywall-with-remote-config)**: A powerful tool that allows you to build a paywall using JSON. With either approach, you'll create impressive paywalls for your users. How you display these paywalls in your mobile app depends on the tool you choose: 1. **Paywalls designed with the Paywall Builder** include both what to display and how to display it. They can also process most user actions such as making purchases, opening links, or clicking buttons without requiring development. Refer to [Design paywalls with Paywall Builder](adapty-paywall-builder) for design details and [iOS](ios-quickstart-paywalls.md), [Android](android-quickstart-paywalls.md), [Flutter](flutter-quickstart-paywalls.md), [React Native](react-native-quickstart-paywalls.md), and [Unity](unity-quickstart-paywalls.md) for implementation guidance. 2. **Paywalls customized using remote config** can be tailored to your specific requirements, giving you complete freedom to design and process them as needed. Refer to [Design paywall with remote config](customize-paywall-with-remote-config) for design details and [iOS](present-remote-config-paywalls.md), [Android](present-remote-config-paywalls-android.md), [Flutter](present-remote-config-paywalls-flutter.md), [React Native](present-remote-config-paywalls-react-native.md), and [Unity](present-remote-config-paywalls-unity.md) for implementation guidance. ## Paywall states Paywalls can have four distinct states: - **Draft**: These paywalls are in the preparation stage and have never been used in any placements or A/B tests. Use this state while you are designing your paywall until you are ready to use it in your mobile app. - **Live**: These paywalls are currently active and running in placements and/or A/B tests. Live paywalls can be used in multiple A/B tests and associated with various placements. You may conduct one or more A/B tests based on a live paywall if it turns out to be effective. - **Inactive**: These paywalls were previously active in placements but are no longer live. You can repurpose an inactive paywall for a new A/B test or choose to [archive it](archive-paywalls) if it is no longer required. - **Archived**: These paywalls are no longer in use and have been archived. You can always [restore the archived paywall to an active state](restore-paywall). :::info Changes to live paywalls are reflected immediately. ::: --- # File: placement-metrics.md --- --- title: "Placement metrics" description: "Analyze placement metrics in Adapty to improve paywall performance." --- With Adapty, you have the flexibility to create and manage multiple placements in your app, each associated with distinct paywalls or A/B tests. This versatility enables you to target specific user segments, experiment with different offers or pricing models, and optimize your app's monetization strategy. To gather valuable insights into the performance of your placements and user engagement with your offers, Adapty tracks various user interactions and transactions related to the displayed paywalls. The robust analytics system captures metrics including views, unique views, purchases, trials, refunds, conversion rates, and revenue. The collected metrics are continuously updated in real-time and can be conveniently accessed and analyzed through Adapty's user-friendly dashboard. You have the freedom to customize the time range for analysis, apply filters based on different parameters, and compare metrics across various placements, user segments, or products. Placement metrics are available on the placements list, where you can get an overview of the performance of all your placements. This high-level view provides aggregated metrics for each placement, allowing you to compare their performance and identify trends. For a more detailed analysis of each placements, you can navigate to the placements detail metrics. On this page, you will find comprehensive metrics specific to the selected placements. These metrics provide deeper insights into how a particular placements is performing, allowing you to assess its effectiveness and make data-driven decisions. ### Metrics controls The system displays the metrics based on the selected time period and organizes them according to the left-side column parameter with four levels of indentation. #### View options for metrics data The placement metrics page offers two view options for metrics data: paywall-based and audience-based. In the paywall-based view, metrics are grouped by placements associated with the paywall. This allows users to analyze metrics by different placements. In the audience-based view, metrics are grouped by the target audience of the paywall. Users can assess metrics specific to different audience segments. #### Profile install date filtration #### Time ranges You can choose from a range of time periods to analyze metrics data, allowing you to focus on specific durations such as days, weeks, months, or custom date ranges. #### Available filters and grouping Adapty offers powerful tools for filtering and customizing metrics analysis to suit your needs. With Adapty's metrics page, you have access to various time ranges, grouping options, and filtering possibilities. - ✅ Filter by: Audience, paywall, paywall group, placement, country, store. - ✅ Group by: Segment, store, and product You can find more information about the available controls, filters, grouping options, and how to use them for paywall analytics in [this documentation.](controls-filters-grouping-compare-proceeds) #### Single metrics chart One of the key components of the placement metrics page is the chart section, which visually represents the selected metrics and facilitates easy analysis. The chart section on the placements metrics page includes a horizontal bar chart that visually represents the chosen metric values. Each bar in the chart corresponds to a metric value and is proportional in size, making it easy to understand the data at a glance. The horizontal line indicates the timeframe being analyzed, and the vertical column displays the numeric values of the metrics. The total value of all the metric values is displayed next to the chart. Additionally, clicking on the arrow icon in the top right corner of the chart section expands the view, displaying the selected metrics on the full line of the chart. #### Total metrics summary Next to the single metrics chart, the total metrics summary section is shown, which displays the cumulative values for the selected metrics at a specific point in time, with the ability for you to change the displayed metric using a dropdown menu. ### Metrics definitions Unlock the power of placement metrics with our comprehensive definitions. From revenue to conversion rates, gain valuable insights that will supercharge your monetization strategies and drive success for your app. #### Revenue This metric represents the total amount of money generated in USD from purchases and renewals within specific placements. Please note that the revenue calculation does not include the Apple App Store or Google Play Store commission and is calculated before deducting any fees. #### Proceeds This metric represents the actual amount of money received by the app owner in USD from purchases and renewals within specific placements after deducting the applicable Apple App Store or Google Play Store commission. It reflects the net revenue that directly contributes to the app's earnings. For more information on how proceeds are calculated, you can refer to the Adapty [documentation.](analytics-cohorts#revenue-vs-proceeds) #### ARPPU ARPPU stands for Average revenue per paying user and measures the average revenue generated per paying user within specific placements. It is calculated as the total revenue divided by the number of unique paying users. For example, if the total revenue is $15,000 and there are 1,000 paying users, the ARPPU would be $15. #### ARPAS ARPAS, or Average revenue per active subscriber, allows you to measure the average revenue generated per active subscriber within specific placements. It is calculated by dividing the total revenue by the number of subscribers who have activated a trial or subscription. For example, if the total revenue is $5,000 and there are 1,000 subscribers, the ARPAS would be $5. This metric helps assess the average monetization potential per subscriber. #### ARPU For onboarding placements only. ARPU is the average revenue per user who viewed the onboarding. It's calculated as total revenue divided by the number of unique viewers. #### Unique CR to purchases The Unique conversion rate to purchases is calculated by dividing the number of purchases within specific placements by the number of unique views. It focuses on the ratio of purchases to the unique number of views, providing insights into the effectiveness of converting unique visitors within specific placements into paying customers. #### CR to purchases The Conversion rate to purchases is calculated by dividing the number of purchases within specific placements by the total number of views of paywalls. It indicates the percentage of views within specific placements that result in purchases, providing insights into the effectiveness of your paywall in converting users into paying customers. #### Unique CR to trials The unique conversion rate to trials is calculated by dividing the number of trials started within specific placements by the number of unique views. It measures the percentage of unique views within specific placements that result in trial activations, providing insights into the effectiveness of your paywall in converting unique visitors into trial users. #### Purchases Purchases represent the cumulative total of various transactions made on the paywall within specific placements. The following transactions are included in this metric (renewals are not included): - New purchases are made directly within specific placements. - Trial conversions of trials that were initially activated within specific placements. - Downgrades, upgrades, and cross-grades of subscriptions made within specific placements. - Subscription restores within specific placements, such as when a subscription is reinstated after expiration without auto-renewal. By considering these different types of transactions, the purchases metric provides a comprehensive view of the overall acquisition and monetization activity within specific placements. #### Trials The trials metric represents the total number of trials that have been activated within specific placements. It reflects the number of users who have initiated trial periods through your paywall within those placements. This metric helps track the effectiveness of your trial offering and can provide insights into user engagement and conversion from trials to paid subscriptions. #### Trials canceled The trials canceled metric represents the number of trials within specific placements in which the auto-renewal feature has been switched off. This occurs when users manually unsubscribe from the trial, indicating their decision not to continue with the subscription after the trial period ends. Tracking trials canceled provides valuable information about user behavior and allows you to understand the rate at which users opt out of the trial within specific placements. #### Refunds The refunds metric represents the number of refunded purchases and subscriptions within specific placements. This includes transactions that have been reversed or refunded due to various reasons, such as customer requests, payment issues, or any other applicable refund policies. #### Refund rate The refund rate is calculated by dividing the number of refunds within specific placements by the number of first-time purchases (renewals are not included). For example, if there are 5 refunds and 1,000 first-time purchases, the refund rate would be 0.5%. #### Views The views metric represents the total number of times the paywall within specific placements has been viewed by users. Each time a user visits the paywall within those placements, it is counted as a separate view. Tracking views helps you understand the level of engagement and user interaction with your paywall, providing insights into user behavior and the effectiveness of your paywall placement and design within specific areas of your app. #### Unique views The unique views metric represents the number of unique instances in which the paywall within specific placements has been viewed by users. Unlike total views, which count each visit as a separate view, unique views count each user's visit to the paywall within those placements only once, regardless of how many times they access it. Tracking unique views helps provide a more accurate measure of user engagement and the reach of your paywall within specific placements, as it focuses on individual users rather than the total number of visits. #### Completions & unique completions For onboarding placements only. Completions count the number of times users complete your onboarding placement, meaning that they go from the first to the last screen. If someone completes it twice, that's two **completions** but one **unique completion**. #### Unique completions rate For onboarding placements only. The unique completion number divided by the unique view number. This metric helps you understand how people engage with onboarding placement and make changes if you notice that people ignore it. --- # File: placements.md --- --- title: "Placements" description: "Manage placements in Adapty to optimize paywall visibility and revenue." --- With Adapty's placement system, you can create and run [paywalls](paywalls), [onboardings](https://adapty.io/docs/onboardings), and [A/B tests](ab-tests) at different points in your app user's journey, such as onboarding flow, app settings, etc. These points are called **Placements**. A placement in your app can manage multiple paywalls, onboardings, or A/B tests at a time, each made for a certain group of users, which we call [Audiences](audience). Moreover, you can experiment with paywalls and onboardings, replacing one with another over time without releasing a new app version. The only thing you hardcode in the mobile app is the placement ID. ## Placements list There are two types of placements: - **Paywall placements** - **Onboarding placements** To view all your placements, go to **Placements** from the Adapty main menu. You will see them categorized by **Paywalls** and **Onboardings** tabs. Each tab offers a comprehensive view of various locations in the user journey where paywalls, onboardings, or A/B tests can appear. Each item in the list corresponds to a specific placement, allowing easy management and modification. You can edit placement details, associate them with the desired paywall, onboarding, or A/B test for a specified audience, or remove unnecessary placements. The numbers in the table reflect the analytics for placements since their activation. From here you can: - [Create a new placement](create-placement) - [Edit an existing placement](edit-placement) - [Delete an existing placement](delete-placement) - Download local fallback [paywalls](https://adapty.io/docs/fallback-paywalls) or [onboardings](https://adapty.io/docs/local-fallback-onboarding). Those for paywalls are especially useful and will be used when a user opens the app and there's no connection with Adapty backend (e.g., no internet connection or in the rare case when the backend is down) and there's no cache on the device. --- # File: posthog.md --- --- title: "PostHog" description: "" --- PostHog is an analytics platform that provides tools for tracking user behavior, visualizing product usage, and analyzing retention. With features like event tracking, user flows, and feature flags, it’s designed to help you better understand and improve your product. Integrating PostHog with Adapty enables seamless tracking of subscription-related events, such as trial starts, renewals, and cancellations. By sending these events to PostHog, you can analyze how subscription changes affect user behavior, evaluate paywall performance, and gain deeper insights into your monetization strategies — all within your existing analytics workflow. ## Integration Characteristics | Integration characteristic | Description | | -------------------------- | ------------------------------------------------------------ | | Schedule | Real-time; events may not appear immediately on the PostHog dashboard. | | Data direction | Adapty events are sent from the Adapty server to the PostHog server. | | Adapty integration point |
  • PostHog and Adapty SDKs in the mobile app code
  • Adapty server
| ## PostHog event structure Adapty sends selected events to PostHog as configured in the **Events names** section on the [PostHog Integration page](https://app.adapty.io/integrations/posthog). Each event is structured like this: ```json showLineNumbers { "distinct_id": "john.doe@example.com", "timestamp": "2025-01-08T11:06:12+00:00", "event": "subscription_started", "properties": { "$set": { "email": "user@example.com", "first_name": "John", "last_name": "Doe", "birthday": "1990-01-01", "gender": "male", "os": "iOS" }, "timezone": "America/New_York", "ip_address": "10.168.1.1", "*": "{{other_event_properties}}" } } ``` Where | **Parameter** | **Type** | **Description** | | --------------- | -------------------- | ------------------------------------------------------------ | | **distinct_id** | String | Unique identifier for the user (e.g., `profile.posthog_distinct_user_id`, `customer_user_id`, or `profile_id`). | | **timestamp** | ISO 8601 date & time | The date and time of the event. | | **event** | String | The name of the event as you defined it in the Events names section of the [PostHog configuration](https://app.adapty.io/integrations/posthog). | | **properties** | Object | Contains the [properties.$set](posthog#propertiesset-parameters) and all the [event-specific properties](messaging#event-properties). Each property is optional and won't be sent to PostHog if missing. | ### properties.$set parameters Each `properties.$set` object parameter is optional and won't be sent to PostHog if missing. | **Parameter** | **Type** | **Description** | | --------------- | -------------------- | ------------------------------------------------------------ | | **email** | String | User's email address. | | **first_name** | String | User's first name. | | **last_name** | String | User's last name. | | **birthday** | String (Date) | User's date of birth. | | **gender** | String | User's gender. | | **os** | String | Operating system of the user's device. | ## Setting up PostHog integration 1. Open the [Integrations -> PostHog](https://app.adapty.io/integrations/posthog) page in the Adapty Dashboard and enable the toggle. 2. Log into the [PostHog Dashboard](https://posthog.com/). 3. Navigate to **Settings -> Project**. 4. In the **Project** window, scroll down to the **Project ID** section and copy the **Project API key**. 5. Paste the API key into the **Project API key** field in the Adapty Dashboard. PostHog doesn’t have a specific Sandbox mode for server-to-server integration. 6. Choose your **PostHog Deployment**: | Option | Description | | ------ | ------------------------------------------------------------ | | us/eu | Default PostHog-hosted deployments. | | Custom | For self-hosted instances. Enter your instance URL in the **PostHog Instance URL** field. | 7. (optional) If you're using a self-hosted PostHog deployment, enter your deployment's address in the **PostHog Instance URL** field. 8. (optional) Tweak settings like **Reporting Proceeds**, **Exclude Historical Events**, **Report User's Currency**, and **Send Trial Price**. Check the [Integration settings](https://adapty.io/docs/configuration#integration-settings) for details on these options. 9. (optional) You can also customize which events are sent to PostHog in the **Events names** section. Disable unwanted events or rename them as needed. 10. Click **Save** to finalize the setup. ## SDK configuration To enable receiving attribution data from PostHog, pass the `distinctId` value to Adapty as shown below: ```swift showLineNumbers do { let distinctId = PostHogSDK.shared.getDistinctId() try await Adapty.setIntegrationIdentifier( key: "posthog_distinct_user_id", value: distinctId ) } catch { // handle the error } ``` ```Kotlin showLineNumbers Adapty.setIntegrationIdentifier("posthog_distinct_user_id", PostHog.distinctId()) { error ->    if (error != null) {        // handle the error    } ``` ```java showLineNumbers Adapty.setIntegrationIdentifier("posthog_distinct_user_id", PostHog.distinctId(), error -> { if (error != null) { // handle the error } }); ``` ```javascript showLineNumbers try { final distinctId = await Posthog().getDistinctId(); await Adapty().setIntegrationIdentifier( key: "posthog_distinct_user_id", value: distinctId, ); } catch (e) { // handle the error } ``` There is no official PostHog SDK for Unity. ```typescript showLineNumbers // ... const posthog = usePostHog(); // ... try { await adapty.setIntegrationIdentifier("posthog_distinct_user_id", posthog.getDistinctId()); } catch (error) { // handle `AdaptyError` } ``` Adapty will now send events to PostHog and receive attribution from it. --- # File: predicted-ltv-and-revenue.md --- --- title: "Predictions in cohorts" description: "Use Adapty’s predictive analytics to forecast LTV and revenue." --- Adapty Predictions are designed to help you answer the following questions: 1. What is the predicted lifetime value (LTV) of your user cohorts? 2. Which cohorts are likely to generate the highest revenue in the future? 3. How much you can invest being aware of the predicted payoff? With Adapty prediction, you can gain valuable insights and make data-driven decisions to optimize your revenue and growth strategy. Adapty's prediction model is a powerful new feature that uses machine learning to help you to get a better understanding of the long-term revenue potential and behavior of your app's users. Using advanced gradient boosting techniques, the LTV prediction model can estimate the total revenue a user is expected to generate during the lifetime as a subscriber. This will help you to make informed decisions about user acquisition, marketing strategies, and product development. Currently, the LTV prediction model provides two types of predictions: predicted LTV and predicted revenue. ### Predictions in cohorts Adapty now offers the ability to predict the lifetime value (LTV) of users and their predicted revenue for subscription-based apps. These predictions can be displayed on the cohort analysis page for 3, 6, and 12 months. However, it is important to note that the model does not currently work for lifetimes and for one-time purchases. Additionally, the accuracy of the LTV model may be lower for very new apps with limited data and for apps that have experienced significant changes in their user traffic. #### Prediction model The prediction model is built using gradient boosting, a highly accurate and efficient machine-learning algorithm that can handle large datasets. By leveraging transaction data from your subscription-based apps, our model can predict the LTV for users a year after their profile was created. The data used for the model is completely anonymized. The model is trained on data from all of your apps within Adapty. However, the predicted values are further refined and tailored based on the specific behavior patterns observed in cohorts associated with each individual app. This post-correction algorithm ensures more accurate predictions, taking into account the unique characteristics of each app. The model achieves a high level of accuracy, with a Mean Absolute Percentage Error (MAPE) of slightly below 10%. This level of precision allows businesses to confidently rely on the model's predictions when making data-driven decisions. Adapty utilizes two distinct gradient-boosting models to forecast LTV: - **Revenue prediction model:** Predicts the total revenue generated by a cohort of users. - **LTV prediction model:** Predicts the average LTV of users in a cohort. The trained models are then used to predict the total revenue and average LTV of each cohort within the selected period (3, 6, 9, or 12 months) after cohort creation. These predictions are verified and constrained to ensure they are consistent with typical user behavior patterns. The predictions are initially generated after 1-3 weeks following the creation of the cohort. This timeframe allows for sufficient data gathering and analysis. Suppose a cohort is created on January 1st. Predictions for that cohort would be available sometime between January 15th and January 29th. After the initial prediction generation, the predictions are then updated daily using the latest transactional data available for the cohort. This frequent updating ensures that the predictions remain current and reflect the most recent behavior of the cohort. The model takes into account a variety of significant statistics pertaining to the cohorts, including revenue generated from past subscription periods, user retention rates, present LTV, subscription type, the proportion of users from Google Play and App Store, and geographic distribution of users by country, among others. These features are meticulously chosen to guarantee that the model captures the most pertinent information required to generate precise forecasts about the future LTV of users. The model typically reflects changes in product performance, such as increased retention for monthly subscriptions, with a delay of approximately one week. The ML model used to predict revenue and LTV has certain limitations that should be taken into account when interpreting its results. These limitations include: - Data quality: The model's performance depends on the quality and representativeness of the data available. Data quality is a crucial aspect of data analysis, and one of the main reasons for insufficient results is when a cohort behaves unusually and deviates from normal cohort metrics for a particular app or all other apps. This type of data is considered uncommon for the model, which can lead to unexpected results. This situation is more common for new, unusual products or new apps that haven't been included in the training set. - Time frame: The model can predict values up to 12 months from the creation of a user cohort. You will see growing actual revenue but the prediction will remain at the point of 12 months. - Subscription durations: The model shows the best performance on monthly and weekly subscriptions #### Prediction in Adapty cohorts To access predicted revenue and predicted LTV values for your subscribers, you can navigate to the Cohort Analyses page in your Adapty dashboard. Also, if you want to learn more about the Adapty cohort, please reference our [documentation](analytics-cohorts) about it. **The predicted revenue (pRevenue)** column shows the estimated total revenue a cohort of subscribers is expected to generate during the selected time frame after cohort creation. This value is calculated using Adapty's revenue prediction model, which utilizes advanced gradient boosting techniques to predict revenue for users. **The predicted LTV (pLTV)** column shows the estimated lifetime value of each user in the selected cohort. This value is calculated by dividing the predicted revenue by the predicted number of paying users in the cohort. The predicted number of paying users is calculated using Adapty's base prediction model, which predicts the number of paying users in a cohort. To define the time period for which the predicted revenue and predicted LTV values are displayed, you can select the desired value from the timeframe dropdown in the user interface. The available options are typically 3, 6, 9, or 12 months after cohort creation. To provide even more valuable insights, Adapty allows you to filter predicted revenue and LTV by product. By default, Adapty builds predictions based on all purchase data, but filtering by product can help you better understand how each product is performing and how it contributes to your overall predicted revenue and LTV. When there is insufficient data available to generate accurate predictions, the corresponding fields will be greyed out in the user interface. Hovering over the greyed-out fields will display a tooltip with the message "**Insufficient data for accurate prediction**". This serves as a visual cue that the predicted values may not be reliable and further data collection and analysis may be necessary to generate accurate predictions. The lack of data may occur due to several reasons, such as insufficient time since cohort creation, a small cohort size, or an unpopular subscription type. In some cases, the cohort may behave unusually, deviating from the normal metrics used to train the model. Waiting a few weeks may help resolve this issue. Also, another possible case is there are no values for the prediction. Empty results may occur if there is either insufficient data to make any predictions (usually at least 1-3 weeks of data is needed), or if the maximum time frame for prediction has already passed, i.e., it has been more than a year since the cohort was created. :::warning When upgrading your pricing plan to the Pro+ or Enterprise, it's important to note that there may be a maximum delay of 24 hours before the prediction data for Revenue and LTV becomes available on your Adapty dashboard. ::: Adapty cohorts provides valuable insights into your revenue and user LTV (lifetime value) by showing current revenue and current money per paying user. By tracking these metrics, you can monitor how your actual numbers are progressing toward the predicted values and track your progress over time. While the primary purpose of using prediction numbers in building plans, it's important to exercise caution and not solely rely on these predictions for decision-making. Rather, they should be used as a guide to inform your strategy and help you make informed business decisions. By leveraging Adapty cohorts and predict, you can gain a better understanding of your revenue and user behavior, and use this information to optimize your business operations for greater success. --- # File: predictions-in-ab-tests.md --- --- title: "Predictions in A/B tests" description: "Learn how predictions in A/B tests help refine subscription pricing strategies." --- Welcome to the Adapty Predictive Analytics documentation for our A/B testing feature. This tool will provide insights into the future results of your running A/B Tests and help you make data-driven decisions faster 🚀 with Adapty's ML-powered predictions. :::note A/B test winner predictions are only available on Pro+ and Enterprise plans ::: ### What are A/B test predictions? Adapty's A/B Test Predictions employ advanced machine learning techniques (specifically gradient boosting models) to forecast the long-term revenue potential of the paywalls that are compared in an A/B test. This predictive model enables you to select the most effective paywall based on projected revenue after a year, instead of relying only on the metrics you observe while the test is running. This allows you decide on the winner more reliably and faster, without having to wait weeks for the data to accumulate. ### How does the model work? The model is trained on extensive historical A/B test data from a variety of apps in different categories. It incorporates a wide range of features to predict the revenue a paywall is likely to generate in a year after the experiment start. These features include: - User transactions and conversion rates over different periods - Geographic distribution of users - Platform usage (iOS or Android) - Opt-out and refund rates - Subscription products and their period lengths (daily, monthly, yearly and so on) - Other transaction-related data The model also accounts for trial periods in paywalls, using historical conversion rates to predict revenue as if users were already converted. This ensures a fair comparison between paywalls with and without trial offers, because we will also account for active trials potentially bringing in revenue in the future. ### How is Predicted P2BB different from just the P2BB? Our A/B tests utilise the Bayesian approach: basically we model the distribution of the revenue per user (or “Revenue per 1K users” to be specific) and then calculate the probability that one distribution is “truly” better than the other one and not by a random chance — and this is what we call the Probability-to-be-the-best or P2BB (you can learn more about our approach [here](maths-behind-it)). It’s important to note that while doing so, we only rely on the revenue that has been accumulated over the time the test has been running. So if you were to run a test comparing a yearly subscription to a weekly one, you would have to wait a really long time to truly understand what performs better. A similar thing happens when you compare trial subscriptions with non-trial subscriptions in an A/B test — as the active trials that could potentially shift the winner dynamics are always unaccounted for in the revenue. This is where our predictive model comes into play. Having the current revenue distribution in an A/B test and trained on a large dataset it’s able to predict the future version of the revenue distribution (namely after 1 year). And after doing so, it produces a predicted P2BB — the one that you would arrive at if you were to run the test for the entire year. Note that sometimes predicted P2BB can contradict the current P2BB. When that's the case, we highlight the variation rows with yellow like so: We consider that a signal that you should accumulate more data to confirm the winner or dig deeper into the A/B test to find out the reason behind it. Generally we recommend trusting the predicted P2BB over the current P2BB because it simply takes more data into account, but the final decision is of course up to you. ### Model accuracy and certainty The model achieves a high level of accuracy, with a Mean Absolute Percentage Error (MAPE) of slightly below 10%. This level of precision allows businesses to confidently rely on the model's predictions when making data-driven decisions. To further ensure stability, the model employs a 'certainty' criterion based on three factors: - A narrow prediction interval - the model is confident in its outcome - Sufficient amount of subscriptions & revenue in the test - At least 2 weeks from the test start have passed To assure the quality of the prediction is of the highest standards possible, prediction is considered reliable only if at least two of these criteria are met without completely failing the third. When a new A/B test begins, the model provides a year-ahead revenue per 1k (our main A/B test metric) prediction for each paywall. Predictions are displayed only when they meet the certainty criteria. If the data is insufficient, the model will indicate "insufficient data for prediction”. ### Limitations and considerations While our predictive model is a powerful tool, it's important to consider its limitations. The model's performance depends on the quality and representativeness of the available data. Unusual cohort behaviour or new apps not included in the training set can affect prediction accuracy. Nevertheless, predictions are updated daily to reflect the latest data and user behaviors. This ensures that the insights you receive are always based on the most current information. 🚧 Note: This tool is a supplement to, not a replacement for, your expert judgment and understanding of your app's unique dynamics. Use these predictions as a guide alongside other metrics and market knowledge to make informed decisions. --- # File: present-remote-config-paywalls-android.md --- --- 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." displayed_sidebar: sdkandroid --- 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`](android-sdk-models#adaptypaywall) object. | --- # File: present-remote-config-paywalls-flutter.md --- --- 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." displayed_sidebar: sdkflutter --- 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`](flutter-sdk-models#adaptypaywall) object. | --- # File: present-remote-config-paywalls-react-native.md --- --- title: "Render paywall designed by remote config in React Native SDK" description: "Discover how to present remote config paywalls in Adapty React Native SDK to personalize user experience." displayed_sidebar: sdkreactnative --- 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" }); const headerText = paywall.remoteConfig?.["header_text"]; } catch (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-react-native#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](react-native-making-purchases). We recommend [creating a backup paywall called a fallback paywall](react-native-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 await adapty.logShowPaywall(paywall); ``` Request parameters: | Parameter | Presence | Description | | :---------- | :------- |:--------------------------------------------------------------------------| | **paywall** | required | An [`AdaptyPaywall`](react-native-sdk-models#adaptypaywall) object. | --- # File: present-remote-config-paywalls-unity.md --- --- title: "Render paywall designed by remote config in Unity SDK" description: "Discover how to present remote config paywalls in Adapty Unity SDK to personalize user experience." displayed_sidebar: sdkunity --- 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. ```csharp showLineNumbers Adapty.GetPaywall("YOUR_PLACEMENT_ID", (result) => { if (result.IsSuccess) { var paywall = result.Value; var headerText = paywall.RemoteConfig?["header_text"] as string; } else { // 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-unity#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](unity-making-purchases). We recommend [creating a backup paywall called a fallback paywall](unity-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). ::: ```csharp showLineNumbers Adapty.LogShowPaywall(paywall, (error) => { // handle the error }); ``` Request parameters: | Parameter | Presence | Description | | :---------- | :------- |:------------------------------------------------------------------| | **paywall** | required | An [`AdaptyPaywall`](unity-sdk-models#adaptypaywall) object. | --- # File: present-remote-config-paywalls.md --- --- 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`](sdk-models#adaptypaywall) object. | --- # File: product.md --- --- title: "Products" description: "Explore Adapty's product settings to configure and optimize in-app purchases and subscriptions." --- A Product is any item or content that is available for purchase within a mobile application. This can include various digital goods such as virtual items, subscriptions, additional features, or content upgrades that users can buy to enhance their experience within the app. For example, in a gaming app, products might include in-game currency, power-ups, or expansion packs. In a productivity app, products might include premium features or access to exclusive content. These products are managed and sold through the in-app purchase system provided by the platform (e.g., Apple App Store or Google Play Store). In Adapty you can combine similar products that you have in App Store and Play Store in a single internal product. This allows you to use a single Adapty product across all platforms, instead of using each vendor's products. Make sure you've [done the initial configuration](quickstart) without skipping a single step and [created the products in the App Store](app-store-products) and/or [created products in Google Play](android-products) before adding them in the Adapty Dashboard. :::note Checklist to successfully display products in your mobile app 1. [Create products in the Adapty Dashboard](create-product). 2. [Create a paywall in the Adapty Dashboard and add products to it](create-paywall) 3. Show paywalls using the placements they belong to in your mobile app: - [iOS](ios-quickstart-paywalls.md) - [Android](android-quickstart-paywalls.md) - [Flutter](flutter-quickstart-paywalls.md) - [React Native](react-native-quickstart-paywalls.md) - [Unity](unity-quickstart-paywalls.md) ::: After you create products in the Adapty Dashboard, they are visible in the **Products** tab of the **[Paywalls and Products](https://app.adapty.io/products)** section. --- # File: profiles-crm.md --- --- title: "Profiles/CRM" description: "Manage user profiles and CRM data in Adapty to enhance audience segmentation." --- Profiles is a CRM for your users. With Profiles, you can: 1. Find a user with any ID you have including email and phone number. 2. Explore the full payment path of a user including billing issues, grace periods, and other [events](events). 3. Analyze user's properties such as subscription state, total revenue/proceeds, and more. 4. Grant the user a subscription. ## Subscription state In a full table of subscribers, you can filter, sort, and find users. The state describes the user state in terms of a subscription and can be: | User **state** | Description | | :--------------------- | :----------------------------------------------------------- | | Subscribed | The user has an active subscription with auto-renewal enabled. | | Auto-renew off | The user turned off auto-renewal but still has access to premium features until the end of the subscription period. | | Subscription cancelled | The user canceled their subscription, and it has fully ended. | | Billing issue | The user couldn’t be charged due to a billing issue, either after their subscription or trial expired. | | Grace period | The user is currently in a grace period due to a billing issue that occurred when attempting to charge them after their subscription or trial expired. | | Active trial | The user has an active subscription that is currently in its trial period. | | Trial cancelled | The user canceled the trial and does not have an active subscription. | | Never subscribed | The user has never subscribed or started a trial and remains a freemium user. | ## User attributes You can send any properties that you want for the user. By default, Adapty sets: | Property | Description | | ---------------- | ------------------------------------------------------------ | | Customer user ID | An identifier of your end user in your system. | | Adapty ID | Internal Adapty identifier of your end user, called Profile ID. | | IDFA | The Identifier for Advertisers, assigned by Apple to a user's device. | | Country | Country of your end user. | | OS | The operating system used by the end user. | | Device | The end-user-visible device model name. | | Install date | The date when the user was first recorded in Adapty:
  • The date the user was created.
  • If the user installed your app before you integrated Adapty, it reflects the date of their first transaction.
  • If applicable, the date provided during a historical data import.
| | Created at | The date the user was created. | For a better understanding of your user, we suggest sending at least your internal user ID or user email. This will help you to find a user. After installing SDK, Adapty automatically collects user events from the payment queue and displays them in a user profile. ## Custom attributes You can see custom attributes that were set either from SDK or manually assign them to the user using the Add attribute button in the Attributes section on the profile page. ## Grant a subscription In a profile, you can find an active subscription. At any time you can prolong the user's subscription or grant lifetime access. It's most useful for users without an active subscription so you can grant the individual user or a group of users premium features for some time. Please note that adjusting the subscription date for active subscriptions will not impact the ongoing payments. :::note **Expires at** must be a date in the future and can't be decreased ones set. ::: ## Profile record creation Adapty creates an internal profile ID for every user. However, if you have your own authentication system, [set your own Customer User ID](identifying-users), a unique identifier for each user in your system. In this case, Adapty will add this ID to the user profile, which will give you several advantages: 1. All transactions and events will be tied to the same profile. 2. You can find users by their customer user ID in the [**Profiles**](profiles-crm) section and view their transactions and events. 3. You can use the customer user ID in the [server-side API](getting-started-with-server-side-api). 4. The customer user ID will be sent to all integrations. If no customer user ID is passed to Adapty, Adapty will create a new additional internal profile ID in the following cases: - When a user launches your app for the first time after installation and reinstallation. - When a user logs out of your app. This means that a user who installs, then uninstalls, and reinstalls your app may have several profile records in Adapty if no customer user ID is used. All transactions in a chain are tied to the profile that generated the first transaction — the "original" profile. This helps keep a complete transaction history — including trial periods, subscription purchases, renewals, and more, linked to the same profile. A new profile record that generates a subsequent transaction, called a "non-original" profile, may not have any events associated with it but will retain the granted access level. In some cases, you will also see "access_level_updated" events here. Here is an example of a non-original profile. Notice the absence of events in the **User history** and the presence of an access level. ## Event timestamps with future dates Why do events show future timestamps in profiles and integrations? Event timestamps may appear with future dates in profiles and integrations because Apple sends renewal events in advance. - **Why it happens**: Apple does this to ensure subscriptions renew automatically before expiring, preventing user service interruptions. For more details, check Apple’s Developer Forum: [Server Notifications for Subscriptions](Server Notifications for Subscriptions). - **Event types affected**: Typically, this applies to subscription renewals and trial-to-paid conversions. These events may have future timestamps because Apple notifies systems about them ahead of time. All other events—like additional in-app purchases or subscription plan changes—are recorded with their actual timestamps since they cannot be predicted in advance. - **Impact on Analytics and Event Feed**: These events will only appear in **Analytics** and the **Event Feed** once their timestamps have passed. Events with future timestamps are not shown in either section. - **Impact on Integrations**: Adapty sends events to integrations as soon as they are received. If an event has a future timestamp, it will be shared with your integration exactly as received. ## Sharing access levels between profiles When a [Customer User ID](identifying-users#setting-customer-user-id-on-configuration) tries to restore transactions or extend a subscription that is already associated with a different identified [Customer User ID](identifying-users#setting-customer-user-id-on-configuration), you can control how Adapty responds by adjusting the **Sharing paid access between user accounts** dropdown in the [Adapty Dashboard -> **App settings** -> **General** tab](https://app.adapty.io/settings/general): ### Access sharing history When access levels are shared or transferred, you might want to know who granted access to the current user or who the current user shared their access with. To find out, just open the user’s **Profile** and click the link to view the connected profile. --- # File: pushwoosh.md --- --- title: "Pushwoosh" description: "Integrate Pushwoosh with Adapty for seamless push notification tracking." --- Adapty uses subscription events to update [Pushwoosh](https://www.pushwoosh.com/) profile tags, so you can build target communication with customers using push notifications after a short and easy integration setting as described below. ## How to set up Pushwoosh integration To integrate Pushwoosh go to [**Integrations** -> **Pushwoosh**](https://app.adapty.io/integrations/pushwoosh), turn on a toggle from off to on, and fill out fields. First of all set credentials to build a connection between your Pushwoosh and Adapty profiles. Pushwoosh app ID and auth token are required. 1. **App ID** can be found in your Pushwoosh dashboard. 2. **Auth token **can be found in the API Access section in Pushwoosh Settings. ## Events and tags Below the credentials, there are three groups of events you can send to Pushwoosh from Adapty. Simply turn on the ones you need. You may also change the names of the events as you need to send it to Pushwoosh. Check the full list of the Events offered by Adapty [here](events). Adapty will send subscription events to Pushwoosh using a server-to-server integration, allowing you to view all subscription events in your Pushwoosh Dashboard. :::note Custom tags With Adapty you can also use your custom tags for Pushwoosh integration. You can refer to the list of tags provided below to determine which tag is best suited for your needs. ::: | Tag | Type | Value | |---|----|-----| | `adapty_customer_user_id` | String | Contains the value of the unique identifier of the user, which can be found on the Pushwoosh side. | | `adapty_profile_id` | String | Contains the value of the unique identifier Adapty User Profile ID of the user, which can be found in your Adapty [dashboard](profiles-crm). | | `environment` | String |

Indicates whether the user is operating in a sandbox or production environment.

Values are either `Sandbox` or `Production`

| | `store` | String |

Contains the name of the Store that used to make the purchase.

Possible values:

`app_store` or `play_store`.

| | `vendor_product_id` | String |

Contains the value of Product ID in the Apple/Google store.

e.g., org.locals.12345

| | `subscription_expires_at` | String |

Contains the expiration date of the latest subscription.

Value format is:

year-month dayThour:minute:second

e.g., 2023-02-10T17:22:03.000000+0000

| | `last_event_type` | String | Indicates the type of the last received event from the list of the standard [Adapty events](events) that you have enabled for the integration. | | `purchase_date` | String |

Contains the date of the last transaction (original purchase or renewal).

Value format is:

year-month dayThour:minute:second

e.g., 2023-02-10T17:22:03.000000+0000

| | `original_purchase_date` | String |

Contains the date of the first purchase according to the transaction.

Value format is:

year-month dayThour:minute:second

e.g., 2023-02-10T17:22:03.000000+0000

| | `active_subscription` | String | The value will be set to `true` on any purchase/renewal event, or `false` if the subscription is expired. | | `period_type` | String |

Indicates the latest period type for the purchase or renewal.

Possible values are

`trial` for a trial period or `normal` for the rest.

| All float values will be rounded to int. Strings stay the same. In addition to the pre-defined list of tags available, it is possible to send [custom attributes](segments#custom-attributes) using tags. This allows for more flexibility in the type of data that can be included with the tag and can be useful for tracking specific information related to a product or service. All custom user attributes are sent automatically to Pushwoosh if the user marks the ** Send user custom attributes** checkbox from[ the integration page](https://app.adapty.io/integrations/pushwoosh) ## SDK configuration To link Adapty with Pushwoosh, you need to send us the `HWID` value: ```swift showLineNumbers do { try await Adapty.setIntegrationIdentifier( key: "pushwoosh_hwid", value: Pushwoosh.sharedInstance().getHWID() ) } catch { // handle the error } ``` ```kotlin showLineNumbers Adapty.setIntegrationIdentifier("pushwoosh_hwid", Pushwoosh.getInstance().hwid) { error -> if (error != null) { // handle the error } } ``` ```java showLineNumbers Adapty.setIntegrationIdentifier("pushwoosh_hwid", Pushwoosh.getInstance().getHwid(), error -> { if (error != null) { // handle the error } }); ``` ```javascript showLineNumbers final hwid = await Pushwoosh.getInstance.getHWID; try { await Adapty().setIntegrationIdentifier( key: "pushwoosh_hwid", value: hwid, ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` ```csharp showLineNumbers using AdaptySDK; Adapty.SetIntegrationIdentifier( "pushwoosh_hwid", Pushwoosh.Instance.HWID, (error) => { // handle the error }); ``` ```typescript showLineNumbers // ... try { await adapty.setIntegrationIdentifier("pushwoosh_hwid", hwid); } catch (error) { // handle `AdaptyError` } ``` --- # File: quickstart-paywalls.md --- --- title: "Add paywall to sell products" description: "Create and design paywalls in Adapty, then add them to placements to show specific audiences targeted storefronts." --- :::info To proceed with this guide, make sure you’ve completed the [store integration](integrate-payments.md) and created at least one product as described in the previous [guide on adding products](quickstart-products.md). ::: In Adapty, **paywalls are the only way to deliver products through your app**. This way you can easily track how different product sets perform across user groups and manage how products are visually presented. To get the most out of Adapty, in this guide we'll create a paywall to sell the product you've added on the previous step. How it works: - **Paywall**: A paywall is a container for one or more products. It can contain a visual paywall created in the paywall builder, product information, or JSON configuration for use in your code. Learn more about [paywalls](paywalls.md). - **Placement**: A placement is a specific point in your app where you show a paywall, onboarding flow, or A/B test. Placements let you target specific [audiences](audience.md) with your paywall. Learn more about [placements](placements.md). :::note Even if you don’t design a paywall with Adapty, you still need to proceed with this guide and create one. This way, you can place your products into it and track monetization analytics. ::: ## 1. Build paywall Creating a paywall takes just a few clicks: 1. Go to [**Paywalls**](https://app.adapty.io/paywalls) in the Adapty main menu. 2. Click **Create paywall**. 3. Enter a **Paywall name**. It's an internal identifier in the Adapty Dashboard. 4. Click **Add product** and pick the products to display on the paywall. 5. Click **Create as a draft**. ### Design paywall The easiest way to design a paywall is to create one with the Adapty no-code builder, which requires no design or coding skills. You can choose from a wide array of professionally designed templates or build a fully custom paywall tailored to your app. :::note If you don't want to use the paywall builder, you can implement paywalls manually using [remote config](customize-paywall-with-remote-config.md) with custom JSON payloads. Learn more about Follow the guide for your platform: [iOS](ios-implement-paywalls-manually.md), [Android](android-implement-paywalls-manually.md), [React Native](react-native-implement-paywalls-manually.md), [Flutter](flutter-implement-paywalls-manually.md), [Unity](unity-implement-paywalls-manually.md).. ::: :::tip If your app is published on the App Store, you can create a unique, high-converting paywall tailored to your app in just seconds. Use the AI generator in the **Builder & Generator** tab. ::: Let's design your first paywall. You can craft engaging paywalls with ease: 1. Open **Builder & Generator** on the paywall page. 2. Click **Build no-code paywall**. 3. Choose a template and confirm your choice. 4. Add and customize elements as needed. 5. Click **Save**. To learn more, go to the detailed article on [Paywall builder](adapty-paywall-builder.md#paywall-elements). ## 2. Add paywall to placement Now you need to create a A placement is a specific point in your app where you show a paywall, onboarding flow, or A/B test. Placements let you target specific [audiences](audience.md) with your paywall. Learn more about [placements](placements.md). with the paywall you just created. Let's start with the most essential one - the onboarding placement. Later, you can add more [meaningful placements](choose-meaningful-placements.md) throughout the user journey. 1. Go to [**Placements**](https://app.adapty.io/placements/paywalls) in the Adapty main menu. 2. Click **Create placement**. 3. Enter a **Placement name** (e.g., `main` or `onboarding`). It's an internal identifier in the Adapty Dashboard. 4. Enter a **Placement ID**. You’ll use this ID in the Adapty SDK to load the placement’s paywall. 5. Click **Run Paywall** and choose the paywall you want to show. 6. Click **Save & publish**. In your app code you hardcode only the placement IDs. Everything else is configured in the Adapty Dashboard and can be changed anytime without an app update. :::tip Adapty gives you flexibility to show different paywalls to various user groups and analyze performance. Learn more about [audiences](audience.md) and [A/B tests](ab-tests.md). ::: ## Next steps After linking your paywall to a placement in Adapty, the next step is to display it on a device. Let’s move on to [integrating the Adapty SDK](quickstart-sdk.md) into your app. --- # File: quickstart-products.md --- --- title: "Add products" description: "Add in‑app products or subscriptions to Adapty and link them to your App Store, Google Play, Stripe, Paddle, or custom‑store listings." --- Before you can use Adapty’s core features, you need to add each product you sell and link it to every store or payment platform you support. This setup allows you to deliver products to users’ devices and track them in analytics later. In Adapty, anything your app sells is a **product**. If the same item exists in the App Store, Google Play, or Stripe, you can group them into a single product in Adapty. Set it up once and manage it across all platforms from one place. Let’s add your first product. ## Add your first product :::tip This quickstart covers the basics you need to create a product. For more details, see the guide on [creating products](create-product.md). ::: Let's say you want to add a monthly subscription as a product. 1. Go to [Products](https://app.adapty.io/products) from the Adapty main menu. 2. Click **Create product** at the top right. 3. Add product details: - **Product name**: The name visible only to you across the Adapty dashboard. - **Access level ID**: The unique identifier that determines which features are unlocked after purchase. If all paid users in your app get access to the same features, you can use the default access level: `premium`. For more complex setups, create additional [access levels](access-level.md). - **Period**: The subscription duration. This must match the period configured in the store. :::note By default, all products have an **Uncategorized** period. Make sure to set the correct one, otherwise there might be problems with granting access to your customers. If the product is not a subscription, use the following options: ::: - **Lifetime**: Use a lifetime period for products that unlock the premium features of the app forever. - **Non-Subscriptions**: For products that are not subscriptions and therefore have no duration, use non-subscriptions. These can unlock additional features. - **Consumables**: Consumable items can be purchased multiple times. They can be used up during the life of the application. Examples are in-game currency and extras. Note that consumable products don't affect access levels.
4. Add store details. Choose your store: - **App Store Product ID**: The unique identifier used to access your product on devices.
Click here to learn where to find the App Store Product ID. 1. Go to **Monetization > Subscriptions** in your [Apple App Store Connect](https://appstoreconnect.apple.com/login) account. 2. Open the **Subscription group** for the purchase. 3. You will see the **Product ID** column for purchases included in the subscription group.
- **Google Play Product ID**: The identifier for the product from the Play Store. - **Base plan ID**: The ID that defines the base plan for the product in the Play Store. - **Legacy fallback product**: A fallback product is used exclusively for apps using older versions of the Adapty SDK (versions 2.5 and below). Specify the value in the following format `:`.
Click here to learn where to find the Google Play Product and Base plan IDs. 1. Go to **Monetize with Play > Products > Subscriptions** in your [Google Play Console](https://play.google.com/console/developers/android/app) account. 2. Open the **Subscription** for the purchase. 3. You will see the Product ID in the **Subscription details** section and the Base plan ID in the **ID and duration** column of the **Base plans and offers** section.
- **Stripe Product ID**: The unique product identifier from Stripe. - **Stripe Price ID**: The unique identifier from Stripe for the price associated with the product.
Click here to learn where to find the Stripe Product and Price IDs. 1. Go to your [Product Catalog](https://dashboard.stripe.com/products?active=true) in Stripe. 2. Open the product you need. 3. You will see: - The Stripe Product ID (looks like `prod_...`) in the top right corner. - The Stripe Price ID (looks like `price_...`) in the **API ID** column of the **Pricing** section.
- **Paddle Product ID**: The unique product identifier from Paddle. - **Paddle Price ID**: The unique identifier from Paddle for the price associated with the product.
Click here to learn where to find the Paddle Product and Price IDs. 1. Go to your [Product Catalog](https://vendors.paddle.com/products-v2) in Paddle. 2. Open the product you need. 3. You will see: - The Paddle Product ID (looks like `pro_...`) in the **Additional details** section. - The Paddle Price ID (looks like `pri_...`) in the **ID** column of the **Prices** section.
You can select an existing custom store or add a new one and associate a product with it. Keep in mind that Adapty only tracks transactions from the App Store, Google Play, and Stripe. For custom stores, you'll need to submit transactions using the Adapty server-side API [Set transaction method](ss-set-transaction.md).
5. Later, you can [create offers](create-offer.md) for the product if needed: promotional offers for the App Store and all other types of offers for other stores. Your product will appear in the product list. ## Next steps Once you've added your products to Adapty, you can move on to [setting up paywalls](quickstart-paywalls.md) as it's the only way to start selling them. --- # File: quickstart-sdk.md --- --- title: "Integrate the Adapty SDK in your app code" description: "Integrate Adapty with App Store, Google Play, custom stores, Stripe, and Paddle." --- Integrate Adapty SDK into your app to: - 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 ## How does it work For the basic implementation of the Adapty SDK, you need to take care of three things only: 1. Install and initialize the SDK. 2. Delegate handling in-app purchases to Adapty. 3. Monitor subscription status in the profile. Adapty determines subscription status, type, and expiration – the SDK just consumes this info. The order and details may vary from app to app, but basically that's it. ## Get started Choose your platform and dive right in: **iOS** - **[SDK Quickstart](ios-sdk-overview.md)** - **[Sample App](https://github.com/adaptyteam/AdaptySDK-iOS/tree/master/Examples)** **Android** - **[SDK Quickstart](android-sdk-overview.md)** - **[Sample App](https://github.com/adaptyteam/AdaptySDK-Android)** **Flutter** - **[SDK Quickstart](flutter-sdk-overview.md)** - **[Sample App](https://github.com/adaptyteam/AdaptySDK-Flutter/tree/master/example)** **React Native** - **[SDK Quickstart](react-native-sdk-overview.md)** - **[Sample App](https://github.com/adaptyteam/AdaptySDK-React-Native/tree/master/examples/AdaptyRnSdkExample)** **Unity** - **[SDK Quickstart](unity-sdk-overview.md)** - **[Sample App](https://github.com/adaptyteam/AdaptySDK-Unity)** ## Next steps Once you've configured the Adapty SDK in the app code, you can move on to [testing the implementation](quickstart-test.md). --- # File: quickstart-test.md --- --- title: "Test your integration with Adapty" description: "Quickly verify your Adapty integration by testing SDK activation, paywall fetching, and in-app purchases on App Store, Google Play, Stripe, and Paddle." --- You're all set! Now make sure your integration works as intended and that you can see your purchases in the Adapty dashboard. Running a test purchase is the best way to verify your integration works end-to-end. Start with an in-app purchase, then validate your results. ## 1. Test in-app purchases Follow the guide based on your store or payment platform. ### App store We recommend using a test account (Sandbox Apple ID) and conducting testing on a real device. To learn more about all testing steps, go to the detailed article on [App Store Sandbox testing](test-purchases-in-sandbox.md). :::warning Test on a real device for the most reliable results. You can optionally test using Sandbox and simulator or perform testing with [TestFlight](test-purchases-with-testflight.md), but we don’t recommend these methods as they are less reliable. ::: ### Google Play Store Create a test user and test your app on a real device. To learn more about all testing steps, go to the detailed article on [Google Play Store testing](testing-on-android.md). :::note Google [recommends](https://support.google.com/googleplay/android-developer/answer/14316361) using a real device for testing. If you do decide to use an emulator, make sure that it has Google Play installed to ensure that your app is functioning properly. ::: ### Stripe Testing purchases on Stripe requires connecting Stripe to Adapty using the API key for Stripe Test mode. Transactions that you make from Stripe's Test mode will be considered Sandbox in Adapty. To learn more about all connection steps, go to the [Stripe integration article](stripe.md#6-test-your-integration). ### Paddle Testing purchases on Paddle requires connecting Paddle to Adapty using the API key for Paddle test environment. Transactions that you make from Paddle's test environment will be considered Test in Adapty. To learn more about all connection steps, go to the [Paddle integration article](paddle.md#4-test-your-integration). ## 2. Validate test purchases After making a test purchase, check for the corresponding transaction in the [**Event Feed**](https://app.adapty.io/event-feed) in the Adapty Dashboard. If the purchase doesn't appear in the **Event Feed**, Adapty is not tracking it. Learn more in the detailed guide on [validating test purchases](validate-test-purchases.md). ## Next steps Congratulations on onboarding Adapty successfully! Now you're ready to grow your in-app purchases. You can proceed the following way: - **[A/B testing](ab-tests.md)**: Experiment with different prices, subscription durations, trial periods, and visual elements to identify the most effective combinations. - **[Analytics](how-adapty-analytics-works.md)**: Dive into detailed monetization metrics to understand user behavior and optimize revenue performance. - **Integrations**: Adapty sends [subscription events](events.md) to third party analytics and attribution tool, such as [Amplitude](amplitude), [AppsFlyer](appsflyer), [Adjust](adjust), [Branch](branch), [Mixpanel](mixpanel), [Facebook Ads](facebook-ads), [AppMetrica](appmetrica), and a custom [Webhook](webhook). --- # File: quickstart.md --- --- title: "Quickstart guide" description: "Integrate Adapty with App Store, Google Play, custom stores, Stripe, and Paddle." --- Welcome to Adapty! You’re about to take the first step toward growing your in-app purchases with the best solution for boosting your app’s revenue. Implementation is straightforward, and this quickstart will guide you through the process. Once you complete this guide: - Adapty will handle in-app purchases with all related logic. - You’ll have the flexibility to show the right paywall at the right time to specific users. - You'll access detailed analytics for in-app purchases. - You can start A/B testing and send subscription events to third-party analytics. You can get started with Adapty in just five simple steps: 1. [**Integrate with stores or payment platforms**](integrate-payments.md): Connect Adapty with App Store, Google Play, Stripe, Paddle, or other stores where you sell products. 2. [**Add products**](quickstart-products.md): Add your in‑app products or subscriptions to Adapty and link them with stores or payment platforms. 3. [**Add paywall to enable purchases**](quickstart-paywalls.md): Add a paywall to enable in-app purchases with Adapty. We'll show you the easiest way to do this. 4. [**Integrate Adapty SDK in your app code**](quickstart-sdk.md): Adapty SDK handles purchases, subscription management, and user identification. 5. [**Test your integration with Adapty**](quickstart-test.md): Make sure your integration works as intended and that you can see your purchases in the Adapty dashboard. --- # File: react-native-check-subscription-status.md --- --- title: "Check subscription status in React Native SDK" description: "Learn how to check subscription status in your React Native app with Adapty." displayed_sidebar: sdkreactnative --- 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.addEventListener('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. ```javascript class SubscriptionManager { private currentProfile: any = null; constructor() { // Listen for profile updates adapty.addEventListener('onLatestProfileLoad', (profile) => { this.currentProfile = profile; // Update UI, unlock content, etc. }); } // Use stored profile instead of calling getProfile() hasAccess(): boolean { return this.currentProfile?.accessLevels?.['premium']?.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. ```javascript 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 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](react-native-quickstart-identify.md) to ensure they can access what they have paid for. --- # File: react-native-deal-with-att.md --- --- title: "Deal with ATT in React Native SDK" description: "Get started with Adapty on React Native to streamline subscription setup and management." displayed_sidebar: sdkreactnative --- 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({ // you can also pass a string value (validated via tsc) if you prefer appTrackingTransparencyStatus: AppTrackingTransparencyStatus.Authorized, }); } catch (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: react-native-display-legacy-pb-paywalls.md --- --- title: "Display legacy Paywall Builder paywalls in React Native SDK" description: "Learn how to display legacy Paywall Builder paywalls in your React Native app with Adapty SDK." displayed_sidebar: sdkreactnative --- This page contains guides for displaying legacy Paywall Builder paywalls in your React Native app. Choose the topic you need: - **[Fetch legacy Paywall Builder paywalls](react-native-get-legacy-pb-paywalls)** - Retrieve legacy paywalls and their configuration - **[Present legacy Paywall Builder paywalls](react-native-present-paywalls-legacy)** - Display legacy paywalls to users - **[Handle legacy paywall events](react-native-handling-events-legacy)** - Manage legacy paywall interactions --- # File: react-native-get-legacy-pb-paywalls.md --- --- title: "Fetch legacy Paywall Builder paywalls in React Native SDK" description: "Retrieve legacy PB paywalls in your React Native app with Adapty SDK." displayed_sidebar: sdkreactnative --- 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 React Native 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](react-native-get-pb-paywalls). - For fetching **Remote config paywalls**, see [Fetch paywalls and products for remote config paywalls](fetch-paywalls-and-products-react-native). :::
Before you start displaying paywalls in your React Native 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-reactnative) in your React Native 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 React Native 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 React Native 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: ```typescript showLineNumbers try { const id = 'YOUR_PLACEMENT_ID'; const locale = 'en'; const paywall = await adapty.getPaywall(id, locale); // the requested paywall } catch (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.

| 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 should hardcode is the placement ID. Response parameters: | Parameter | Description | | :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | An [`AdaptyPaywall`](react-native-sdk-models#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-react-native). For React Native, the view configuration is automatically handled when you present the paywall using the `adaptyUI.showPaywall()` method. --- # File: react-native-get-onboardings.md --- --- title: "Get onboardings in React Native SDK" description: "Learn how to retrieve onboardings in Adapty for React Native." slug: /react-native-get-onboardings displayed_sidebar: sdkreactnative --- 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 React Native 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 React Native SDK](sdk-installation-reactnative.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: ```typescript showLineNumbers try { const placementId = 'YOUR_PLACEMENT_ID'; const locale = 'en'; const onboarding = await adapty.getOnboarding(placementId, locale); // the requested onboarding } catch (error) { // handle the 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. Calling it twice without recreating may result in the `AdaptyUIError.viewAlreadyPresented` error. ::: ```typescript showLineNumbers if (onboarding.hasViewConfiguration) { try { const view = await createOnboardingView(onboarding); } catch (error) { // handle the error } } else { //use your custom logic } ``` 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.

| | **loadTimeoutMs** | 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`](sdk-models#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 placementId = 'YOUR_PLACEMENT_ID'; const locale = 'en'; const onboarding = await adapty.getOnboardingForDefaultAudience(placementId, locale); // the requested onboarding } 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 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: react-native-get-pb-paywalls.md --- --- title: "Fetch Paywall Builder paywalls and their configuration in React Native SDK" description: "Learn how to retrieve PB paywalls in Adapty for better subscription control in your React Native app." displayed_sidebar: sdkreactnative --- 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 React Native 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](react-native-legacy.md). ::: 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-react-native) 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-reactnative.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](react-native-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 placementId = 'YOUR_PLACEMENT_ID'; const locale = 'en'; const paywall = await adapty.getPaywall(placementId, locale); // 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.

| | **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.

| | **loadTimeoutMs** | 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! 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 should hardcode is the placement ID. Response parameters: | Parameter | Description | | :-------- |:---------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Paywall | An [`AdaptyPaywall`](react-native-sdk-models#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-react-native). In React Native 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. Calling it twice without recreating may result in the `AdaptyUIError.viewAlreadyPresented` error. ::: ```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](react-native-localizations-and-locale-codes). ::: Once you have successfully loaded the paywall and its view configuration, you can present it in your mobile app. ## 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). ::: ```typescript showLineNumbers try { const id = 'YOUR_PLACEMENT_ID'; const locale = 'en'; const paywall = await adapty.getPaywallForDefaultAudience(id, locale); // the requested paywall } catch (error) { // handle the error } ``` :::note The `getPaywallForDefaultAudience` method is available starting from React Native 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](react-native-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 React Native SDK to version 3.8.0 or higher. ::: Here’s an example of how you can provide custom asssets via a simple dictionary: ```javascript 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: react-native-handle-errors.md --- --- title: "Handle errors in React Native SDK" description: "Handle errors in React Native SDK." --- Every error is returned by the SDK is `AdaptyErrorCode`. Here is an example: :::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. ::: ```typescript showLineNumbers try { const params: MakePurchaseParamsInput = {}; await adapty.makePurchase(product, params); } catch (error) { if ( error instanceof AdaptyError && error.adaptyCode === getErrorCode(ErrorCode['2']) ) { // payment cancelled } } ``` ##  System StoreKit codes | Error | Code | Solution | |-----|----|-----------| | [unknown](https://developer.apple.com/documentation/storekit/skerror/code/unknown) | 0 | Error code indicating that an unknown or unexpected error occurred.
Retry or see the [Other issues](#other-issues) section. | | [clientInvalid](https://developer.apple.com/documentation/storekit/skerror/code/clientinvalid) | 1 | This error code indicates that the client is not allowed to perform the attempted action. | | [paymentCancelled](https://developer.apple.com/documentation/storekit/skerror/code/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](https://developer.apple.com/documentation/storekit/skerror/code/paymentinvalid) | 3 | This error indicates that one of the payment parameters was not recognized by the App Store. | | [paymentNotAllowed](https://developer.apple.com/documentation/storekit/skerror/code/paymentnotallowed) | 4 | This error code indicates that the user is not allowed to authorize payments. | | [storeProductNotAvailable](https://developer.apple.com/documentation/storekit/skerror/code/storeproductnotavailable) | 5 | This error code indicates that the requested product is not available in the store.
Try re-installing the app. | | [cloudServicePermissionDenied](https://developer.apple.com/documentation/storekit/skerror/code/cloudservicepermissiondenied) | 6 | This error code indicates that the user has not allowed access to Cloud service information. | | [cloudServiceNetworkConnectionFailed](https://developer.apple.com/documentation/storekit/skerror/code/cloudservicenetworkconnectionfailed) | 7 | This error code indicates that the device could not connect to the network. | | [cloudServiceRevoked](https://developer.apple.com/documentation/storekit/skerror/code/cloudservicerevoked/) | 8 | This error code indicates that the user has revoked permission to use this cloud service. | | [privacyAcknowledgementRequired](https://developer.apple.com/documentation/storekit/skerror/code/privacyacknowledgementrequired) | 9 | This error code indicates that the user has not yet acknowledged Apple’s privacy policy. | | [unauthorizedRequestData](https://developer.apple.com/documentation/storekit/skerror/code/unauthorizedrequestdata) | 10 | This error code indicates that the app is attempting to use a property for which it does not have the required entitlement. | | [invalidOfferIdentifier](https://developer.apple.com/documentation/storekit/skerror/code/invalidofferidentifier) | 11 |

The offer [`identifier`](https://developer.apple.com/documentation/storekit/skpaymentdiscount/3043528-identifier) is not valid. For example, you have not set up an offer with that identifier in the App Store, or you have revoked the offer.

Make sure you set up desired offers in AppStore Connect and pass a valid offer identifier.

| | [invalidSignature](https://developer.apple.com/documentation/storekit/skerror/code/invalidsignature) | 12 | This error code indicates that the signature in a payment discount is not valid. | | [missingOfferParams](https://developer.apple.com/documentation/storekit/skerror/code/missingofferparams) | 13 | This error code indicates that parameters are missing in a payment discount. | | [invalidOfferPrice](https://developer.apple.com/documentation/storekit/skerror/code/invalidofferprice/) | 14 | This error code indicates that the price you specified in App Store Connect is no longer valid. Offers must always represent a discounted price. | ## Custom Android codes | Error | Code | Solution | |-----|----|-----------| | 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 | Solution | |-----|----|-----------| | noProductIDsFound | 1000 |

This error 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 this error, follow the steps in the [Fix for Code-1000 `noProductIDsFound` error](InvalidProductIdentifiers-react-native) section.

| | 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. See the troubleshooting [guide](cantMakePayments-react-native). | | 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 | Solution | | :------------------- | :--- |:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | notActivated | 2002 | The Adapty SDK is not activated. You need to properly [configure Adapty SDK](sdk-installation-reactnative#configure-adapty-sdk) using the `Adapty.activate` method. | | badRequest | 2003 | Bad request.
Ensure you've completed all the steps required to [integrate with the App Store](app-store-connection-configuration). | | serverError | 2004 | Server error.
Try again after some time. If the issue is not resolved, contact the Adapty support team. | | networkFailed | 2005 | The error indicates issues with the network connection on the user's device.
Try disabling VPN or switching to WiFi from a cellular network or vice versa. | | decodingFailed | 2006 | This error indicates that response decoding failed.
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. | | encodingFailed | 2009 | This error indicates that request encoding failed. | | missingURL | 2010 | The requested URL is nil. | | analyticsDisabled | 3000 | We can't handle analytics events, since you've [opted it out](analytics-integration#disabling-external-analytics-for-a-specific-customer). | | wrongParam | 3001 | This error indicates that some of your parameters are not correct.
If you're using the Adapty paywall builder and can't display a paywall because of this error, toggle on **Show on device** in the paywall builder.
Another possible reason for this issue is that the local [fallback](fallback-paywalls) file version doesn't match the SDK version. Download a new file in the dashboard. | | activateOnceError | 3005 | It is not possible to call `.activate` method more than once. | | profileWasChanged | 3006 | The user profile was changed during the operation.
This error can occur when you call `identify`, and then call another method before `identify` succeds. To avoid it, wait untill `identify` suceeds before calling other methods. | | unsupportedData | 3007 | This error indicates that the data format is not supported by the SDK. | | persistingDataError | 3100 | It was an error while saving data. | | fetchTimeoutError | 3101 | This error indicates that the fetch operation timed out. | ## 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 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-reactnative#logging) and share logs with the team. You can also attach relevant code snippets. --- # File: react-native-handle-paywall-actions.md --- --- title: "Respond to button actions in React Native SDK" description: "Handle paywall button actions in React Native using Adapty for better app monetization." toc_max_heading_level: 4 --- 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 `close` action that dismisses the paywall. :::info In the React Native 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. ::: ```javascript const view = await createPaywallView(paywall); const unsubscribe = view.registerEventHandlers({ onCloseButtonPress() { view.dismiss(); // default behavior return true; } }); ``` ## 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 React Native SDK, the `openUrl` action triggers opening the URL by default. However, you can override this behavior in your code if needed. ::: ```javascript const view = await createPaywallView(paywall); const unsubscribe = view.registerEventHandlers({ onUrlPress(url) { Linking.openURL(url); }, }); ``` ## 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. ```javascript const view = await createPaywallView(paywall); const unsubscribe = view.registerEventHandlers({ onCustomAction(actionId) { if (actionId === 'login') { navigation.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: ```javascript const unsubscribe = view.registerEventHandlers({ onCustomAction(actionId) { if (actionId === 'openNewPaywall') { // Display another paywall } }, }); ``` --- # File: react-native-handling-events-1.md --- --- title: "React Native - Handle paywall events" description: "Handle subscription events in React Native with Adapty's SDK." toc_max_heading_level: 4 --- :::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](react-native-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](react-native-handling-events-legacy). ::: To control or monitor processes occurring on the paywall screen within your mobile app, implement the `view.registerEventHandlers` method: ```javascript showLineNumbers title="React Native (TSX)" const view = await createPaywallView(paywall); const unsubscribe = view.registerEventHandlers({ onCloseButtonPress() { return true; }, onAndroidSystemBack() { return true; }, onPurchaseCompleted(purchaseResult, product) { return purchaseResult.type !== 'user_cancelled'; }, onPurchaseStarted(product) { /***/}, onPurchaseFailed(error) { /***/ }, onRestoreCompleted(profile) { /***/ }, onRestoreFailed(error, product) { /***/ }, onProductSelected(productId) { /***/}, onRenderingFailed(error) { /***/ }, onLoadingProductsFailed(error) { /***/ }, onUrlPress(url) { Linking.openURL(url); return false; // Keep paywall open }, }); ```
Event examples (Click to expand) ```javascript // onCloseButtonPress { "event": "close_button_press" } // onAndroidSystemBack { "event": "android_system_back" } // 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. ### Event handlers | Event handler | Description | | :-------------------------- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **onCustomAction** | If a user performs some custom action, e.g. clicks a [custom button](paywall-buttons), this method will be invoked. | | **onUrlPress** | If a user clicks a URL in your paywall, this method will be invoked. | | **onAndroidSystemBack** | If a user taps the system Android **Back** button, this method will be invoked. | | **onCloseButtonPress** | If the close button is visible and a user taps it, this method will be invoked. It is recommended to dismiss the paywall screen in this handler. | | **onPurchaseCompleted** | If the purchase succeeds, the user cancels their purchase, or the purchase appears to be pending, this method will be invoked. In case of a successful purchase, it will provide an updated `AdaptyProfile`. | | **onPurchaseStarted** | If a user taps the "Purchase" action button to start the purchase process, this method will be invoked. | | **onPurchaseCancelled** | If a user initiates the purchase process and manually interrupts it, this method will be invoked. | | **onPurchaseFailed** | If the purchase process fails, this method will be invoked and provide `AdaptyError`. | | **onRestoreStarted** | If a user starts a purchase restoration, this method will be invoked. | | **onRestoreCompleted** | If a user's purchase restoration succeeds, this method will be invoked and provide an updated `AdaptyProfile`. It is recommended to dismiss the screen if the user has the required `accessLevel`. Refer to the [Subscription status](react-native-listen-subscription-changes) topic to learn how to check it. | | **onRestoreFailed** | If the restoring process fails, this method will be invoked and will provide `AdaptyError`. | | **onProductSelected** | When any product in the paywall view is selected, this method will be invoked, so that you can monitor what the user selects before the purchase. | | **onRenderingFailed** | If an error occurs during view rendering, this method will be invoked and provide `AdaptyError`. Such errors should not occur, so if you come across one, please let us know. | | **onLoadingProductsFailed** | If you haven't set `prefetchProducts: true` in view creation, AdaptyUI will retrieve the necessary objects from the server by itself. If this operation fails, this method will be invoked and provide `AdaptyError`. | --- # File: react-native-handling-events-legacy.md --- --- title: "Handle paywall events in legacy React Native SDK" description: "Handle subscription-related events in React Native (Legacy) with Adapty's event tracking system." toc_max_heading_level: 4 --- 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 covers the process for **legacy Paywall Builder paywalls** only which requires Adapty SDK up to v2.x. For presenting paywalls in Adapty SDK v3.0 or later designed with the new Paywall Builder, see [React Native - Handle paywall events designed with new Paywall Builder](react-native-handling-events-1). ::: To control or monitor processes occurring on the paywall screen within your mobile app, implement the`view.registerEventHandlers` method: ```typescript showLineNumbers title="React Native (TSX)" const view = await createPaywallView(paywall); const unsubscribe = view.registerEventHandlers({ onCloseButtonPress() { return true; }, onPurchaseCompleted(profile) { return true; }, onPurchaseStarted(product) { /***/}, onPurchaseCancelled(product) { /***/ }, onPurchaseFailed(error) { /***/ }, onRestoreCompleted(profile) { /***/ }, onRestoreFailed(error) { /***/ }, onProductSelected() { /***/}, onRenderingFailed(error) { /***/ }, onLoadingProductsFailed(error) { /***/ }, onUrlPress(url) { /* handle url */ }, }); ``` You can register event handlers you need, and miss those you do not need. In this case, unused event listeners would not be created. Note that at the very least you need to implement the reactions to both `onCloseButtonPress` and `onUrlPress`. 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. Note, that `onCloseButtonPress`, `onPurchaseCompleted` and `onRestoreCompleted` in the example above return `true` — This is their default behavior that you can override. ### Event handlers | Event handler | Description | | :-------------------------- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **onCloseButtonPress** | If the close button is visible and a user taps it, this method will be invoked. It is recommended to dismiss the paywall screen in this handler. | | **onPurchaseCompleted** | If a user's purchase succeeds, this method will be invoked and will provide updated `AdaptyProfile`. It is recommended to dismiss the paywall view in this handler. | | **onPurchaseStarted** | If a user taps the "Purchase" action button to start the purchase process, this method will be invoked and will provide `AdaptyPaywallProduct`. | | **onPurchaseCancelled** | If a user initiates the purchase process and manually interrupts it, this method will be invoked and will provide `AdaptyPaywallProduct`. | | **onPurchaseFailed** | If the purchase process fails, this method will be invoked and provide `AdaptyError`. | | **onRestoreCompleted** | If a user's purchase restoration succeeds, this method will be invoked and provide updated `AdaptyProfile`. It is recommended to dismiss the screen if the user has the required `accessLevel`. Refer to the [Subscription status](react-native-listen-subscription-changes) topic to learn how to check it. | | **onRestoreFailed** | If the restoring process fails, this method will be invoked and will provide `AdaptyError`. | | **onProductSelected** | When any product in the paywall view is selected, this method will be invoked, so that you can monitor what the user selects before the purchase. | | **onRenderingFailed** | If an error occurs during view rendering, this method will be invoked and provide `AdaptyError`. Such errors should not occur, so if you come across one, please let us know. | | **onLoadingProductsFailed** | If you haven't set `prefetchProducts: true` in view creation, AdaptyUI will retrieve the necessary objects from the server by itself. If this operation fails, this method will be invoked and provide `AdaptyError`. | --- # File: react-native-handling-onboarding-events.md --- --- title: "Handle onboarding events in React Native SDK" description: "Handle onboarding-related events in React Native using Adapty." toc_max_heading_level: 4 --- 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 event handlers that handle 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 React Native SDK](sdk-installation-reactnative.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 handlers To handle events for full-screen onboardings, use the `view.registerEventHandlers` method: ```javascript showLineNumbers title="React Native" const view = await createOnboardingView(onboarding); const unsubscribe = view.registerEventHandlers({ onAnalytics(event, meta) { // Track analytics events }, onClose(actionId, meta) { // Handle close action view.dismiss(); return true; }, onCustom(actionId, meta) { // Handle custom actions }, onPaywall(actionId, meta) { // Handle paywall actions }, onStateUpdated(action, meta) { // Handle user input updates }, onFinishedLoading(meta) { // Onboarding finished loading }, onError(error) { // Handle loading errors }, }); try { await view.present(); } catch (error) { // handle the error } ``` ## Embedded widget events When using `AdaptyOnboardingView`, you can handle events through inline callback parameters directly in the widget: ```javascript showLineNumbers title="React Native" ``` ## 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 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". ```javascript showLineNumbers title="React Native" // Full-screen presentation const unsubscribe = view.registerEventHandlers({ onCustom(actionId, meta) { switch (actionId) { case 'login': login(); break; case 'allow_notifications': allowNotifications(); break; } }, }); // Embedded widget ```
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: ```javascript showLineNumbers title="React Native" // Full-screen presentation const unsubscribe = view.registerEventHandlers({ onFinishedLoading(meta) { console.log('Onboarding loaded:', meta.onboardingId); }, }); // Embedded widget ```
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="React Native" // Full-screen presentation const unsubscribe = view.registerEventHandlers({ onClose(actionId, meta) { await view.dismiss(); return true; }, }); // Embedded widget ```
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: ```javascript showLineNumbers title="React Native" // Full-screen presentation const unsubscribe = view.registerEventHandlers({ onPaywall(actionId, meta) { openPaywall(actionId); }, }); const openPaywall = async (actionId) => { // Implement your paywall opening logic here }; // Embedded widget ```
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 } } ```
### Updating field state When your users respond to a quiz question or input their data into an input field, the state update event will be triggered: ```javascript showLineNumbers title="React Native" // Full-screen presentation const unsubscribe = view.registerEventHandlers({ onStateUpdated(action, meta) { saveUserResponse(action.elementId, action.params); }, }); // Embedded widget ``` :::note If you want to save or process data, you need to implement the methods yourself. ::: The `action` object contains: - `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, which can be one of the following types: - `select`: Single selection from a list of options. - `multiSelect`: Multiple selections from a list of options. - `input`: Text input from the user. - `datePicker`: Date selected by the user.
Saved data examples (Click to expand) ```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 } } } ```
### Tracking navigation You receive an analytics event when various navigation-related events occur during the onboarding flow: ```javascript showLineNumbers title="React Native" // Full-screen presentation const unsubscribe = view.registerEventHandlers({ onAnalytics(event, meta) { trackEvent(event.type, meta.onboardingId); }, }); // Embedded widget ``` 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: react-native-identifying-users.md --- --- title: "Identify users in React Native SDK" description: "Learn how to identify users in your React Native app with Adapty SDK." slug: /react-native-identifying-users displayed_sidebar: sdkreactnative --- 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 adapty.activate("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. ```typescript showLineNumbers try { await adapty.identify("YOUR_USER_ID"); // successfully identified } catch (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: ```typescript showLineNumbers try { await adapty.logout(); // successful logout } catch (error) { // handle the error } ``` You can then login the user using `.identify()` method. --- # File: react-native-implement-paywalls-manually.md --- --- title: "Implement paywalls manually in React Native SDK" description: "Learn how to implement paywalls manually in your React Native app with Adapty SDK." displayed_sidebar: sdkreactnative --- This page contains guides for implementing paywalls manually in your React Native app. Choose the topic you need: - **[Fetch paywalls and products](fetch-paywalls-and-products-react-native)** - Retrieve paywalls and product data - **[Present remote config paywalls](present-remote-config-paywalls-react-native)** - Display remote config paywalls - **[Accept purchases](react-native-making-purchases)** - Handle purchase transactions - **[Restore purchases](react-native-restore-purchase)** - Restore previous purchases - **[Implement Observer mode](implement-observer-mode-react-native)** - Set up Observer mode for analytics and paywall integration - **[Report transactions in Observer Mode](report-transactions-observer-mode-react-native)** - Report purchase transactions in Observer Mode - **[Troubleshooting](react-native-troubleshoot-purchases)** - Resolve common purchase issues --- # File: react-native-legacy-install.md --- --- title: "Legacy installation guide" description: "Legacy installation guide for React Native Adapty SDK." slug: /react-native-legacy-install displayed_sidebar: sdkreactnative --- 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. You currently need to have a `react-native-adapty` of version 2.4.7 or higher to use UI SDK. Please consult the compatibility table below to choose the correct pair of Adapty SDK and AdaptyUI SDK. | Adapty SDK version | AdaptyUI version | | :----------------- | :--------------- | | 2.7.0 – 2.9.2 | 2.0.0 - 2.0.1 | | 2.9.3 - 2.9.8 | 2.1.0 | | 2.10.0 | 2.1.1 | | 2.10.1 | 2.1.2 | | 2.11.2 | 2.11.0 | | 2.11.3 | 2.11.1 | | 3.0.1 | 3.0.0-3.0.1 | | 3.1.0 | 3.1.0 | :::danger Go through the 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 Adapty SDKs Currently, React Native provides two development paths: Expo and Pure React Native. Adapty seamlessly integrates with both. Please refer to the section below that matches your chosen setup. ### Install Adapty SDKs for Expo React Native You can streamline your development process with Expo Application Services (EAS). While configuration may vary based on your setup, here you'll find the most common and straightforward setup available: 1. If you haven't installed the EAS Command-Line Interface (CLI) yet, you can do so by using the following command: ```sh showLineNumbers title="Shell" npm install -g eas-cli ``` 2. In the root of your project, install the dev client to make a development build: ```sh showLineNumbers title="Shell" expo install expo-dev-client ``` 3. Run the installation command: ```sh showLineNumbers title="Shell" expo install react-native-adapty expo install @adapty/react-native-ui ``` 4. For iOS: Make an iOS build with EAS CLI. This command may prompt you for additional info. You can refer to [expo official documentation](https://docs.expo.dev/develop/development-builds/create-a-build/) for more details: ```sh showLineNumbers title="Shell" eas build --profile development --platform ios ``` 4. For Android: Make an Android build with EAS CLI. This command may prompt you for additional info. You can refer to [expo official documentation](https://docs.expo.dev/develop/development-builds/create-a-build/) for more details: ```sh showLineNumbers title="Shell" eas build --profile development --platform android ``` 5. Start a development server with the following command: ```sh showLineNumbers title="Shell" expo start --dev-client ``` This should result in the working app with react-native-adapty. Possible errors: | Error | Description | |-----|-----------| | `Failed to start (Invariant Violation: Native module cannot be null)` |

if you scan a QR code from a CLI dev client it might lead you to this error. To resolve it you can try the following:

> On your device open EAS built app (it should provide some Expo screen) and manually insert the URL that Expo provides (screenshot below). You can unescape special characters in URL with the JS function `unescape(“string”)`, which should result in something like `http://192.168.1.35:8081`

| ### Install Adapty SDKs with Pure React Native If you opt for a purely native approach to manage React Native purchases, please consult the following instructions: 1. In your project, run the installation command: ```sh showLineNumbers title="Shell" yarn add react-native-adapty yarn add @adapty/react-native-ui ``` 2. For iOS: Install required pods: ```sh showLineNumbers title="Shell" pod install --project-directory=ios pod install --project-directory=ios/ ``` The minimum supported iOS version is 13.0, but the [new Paywall Builder](adapty-paywall-builder) requires iOS 15.0 or higher. If you run into an error during pod installation, find this line in your `ios/Podfile` and update the minimum target. After that, you should be able to run `pod install` without any issues. ```diff showLineNumbers title="Podfile" -platform :ios, min_ios_version_supported +platform :ios, 15.0 ``` 2. For Android: 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" } } ... ``` ## Configure Adapty SDKs To use Adapty SDKs, import `adapty` and invoke `activate` in your _core component_ such as `App.tsx`. Preferably, position the activation before the React component to ensure no other Adapty calls occur before the activation. ```typescript showLineNumbers title="/src/App.tsx" adapty.activate('PUBLIC_SDK_KEY', { observerMode: false, customerUserId: 'YOUR_USER_ID', logLevel: LogLevel.ERROR, __debugDeferActivation: false, ipAddressCollectionDisabled: false, ios: { idfaCollectionDisabled: false, }, activateUi: true, // NOT necessary as the default value is `true`, but you can pass `false` if you don't use the Paywall Builder mediaCache: { memoryStorageTotalCostLimit: 100 * 1024 * 1024, // 100MB memoryStorageCountLimit: 2147483647, // 2^31 - 1 diskStorageSizeLimit: 100 * 1024 * 1024, // 100MB }, }); const App = () => { // ... } ``` Activation parameters: | Parameter | Presence | Description | |---------|--------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | apiKey | required |

A Public SDK Key is the unique identifier used to integrate Adapty into your mobile app. You can copy it in the Adapty Dashboard: [**App settings** -> **General **tab -> **API Keys** section](https://app.adapty.io/settings/general).

**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 the Adapty initialization, since the **Secret key** should be used for the [server-side API](getting-started-with-server-side-api) only.

| | 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 a user in your system. We send it with subscription and analytical events, so we can match events to the right user profile. You can also find customers using the `customerUserId` in the [Profiles](profiles-crm) section.

If you don't have a user ID when you start with Adapty, you can add it later using the `adapty.identify()` method. For more details, see the [Identifying users](react-native-identifying-users) section.

| | logLevel | optional | A string parameter that makes Adapty record errors and other important information to help you understand what's happening. | | \_\_debugDeferActivation | optional | A boolean parameter, that lets you delay SDK activation until your next Adapty call. This is intended solely for development purposes and **should not be used in production**. | | ipAddressCollectionDisabled | optional |

Set to `true` to disable user IP address collection and 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.

| | 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. | 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](making-purchases) within your app. --- # File: react-native-legacy.md --- --- title: "Legacy guides for React Native SDK" description: "Legacy documentation for Adapty React Native SDK." displayed_sidebar: sdkreactnative --- This page contains legacy documentation for Adapty React Native SDK. Choose the topic you need: - **[Legacy installation guide](react-native-legacy-install)** - Install and configure legacy React Native SDK - **[Display legacy Paywall Builder paywalls](react-native-display-legacy-pb-paywalls)** - Work with legacy paywall builder --- # File: react-native-listen-subscription-changes.md --- --- title: "Check subscription status in React Native SDK" description: "Track and manage user subscription status in Adapty for improved customer retention in your React Native 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](sdk-models#adaptyprofile) object. We recommend retrieving the profile when your app starts, such as when you [identify a user](react-native-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](react-native-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(); } catch (error) { // handle the error } ``` Response parameters: | Parameter | Description | | --------- | ------------------------------------------------------------ | | Profile |

An [AdaptyProfile](sdk-models#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 } } catch (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: ```typescript showLineNumbers // Create an "onLatestProfileLoad" event listener adapty.addEventListener('onLatestProfileLoad', 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: react-native-localizations-and-locale-codes.md --- --- title: "Use localizations and locale codes in React Native SDK" description: "Learn how to localize paywalls in your React Native app with Adapty SDK." slug: /react-native-localizations-and-locale-codes displayed_sidebar: sdkreactnative --- ## 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: ```javascript showLineNumbers // 1. Modify your localization files (e.g., using react-i18next) /* en.json */ { "adapty_paywalls_locale": "en" } /* es.json */ { "adapty_paywalls_locale": "es" } /* pt-BR.json */ { "adapty_paywalls_locale": "pt-br" } // 2. Extract and use the locale code const MyComponent = () => { const { t } = useTranslation(); const fetchPaywall = async () => { const locale = t('adapty_paywalls_locale'); // pass locale code to adapty.getPaywall or adapty.getPaywallForDefaultAudience method const paywall = await adapty.getPaywallForDefaultAudience('placement_id', locale); }; }; ``` 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: ```javascript showLineNumbers const getLocaleCode = () => { if (Platform.OS === 'ios') { return NativeModules.SettingsManager.settings.AppleLocale || NativeModules.SettingsManager.settings.AppleLanguages[0]; } else { return NativeModules.I18nManager.localeIdentifier; } }; const fetchPaywall = async () => { const locale = getLocaleCode(); // pass locale code to adapty.getPaywall or adapty.getPaywallForDefaultAudience method const paywall = await adapty.getPaywallForDefaultAudience('placement_id', locale); }; ``` 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: react-native-making-purchases.md --- --- title: "Make purchases in mobile app in React Native SDK" description: "Guide on handling in-app purchases and subscriptions using Adapty." displayed_sidebar: sdkreactnative --- 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-react-native#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 In paywalls built with [Paywall Builder](adapty-paywall-builder) purchases are processed automatically with no additional code. If that's your case — you can skip this step. ::: ```typescript showLineNumbers try { const purchaseResult = await adapty.makePurchase(product); switch (purchaseResult.type) { case 'success': const isSubscribed = purchaseResult.profile?.accessLevels['YOUR_ACCESS_LEVEL']?.isActive; if (isSubscribed) { // Grant access to the paid features } break; case 'user_cancelled': // Handle the case where the user canceled the purchase break; case 'pending': // Handle deferred purchases (e.g., the user will pay offline with cash) break; } } catch (error) { // Handle the error } ``` Request parameters: | Parameter | Presence | Description | | :---------- | :------- | :-------------------------------------------------------------------------------------------------- | | **Product** | required | An [`AdaptyPaywallProduct`](sdk-models#adaptypaywallproduct) object retrieved from the paywall. | Response parameters: | Parameter | Description | |---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Profile** |

If the request has been successful, the response contains this object. An [AdaptyProfile](sdk-models#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 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: ```typescript showLineNumbers try { const purchaseResult = await adapty.makePurchase(product, params); switch (purchaseResult.type) { case 'success': const isSubscribed = purchaseResult.profile?.accessLevels['YOUR_ACCESS_LEVEL']?.isActive; if (isSubscribed) { // Grant access to the paid features } break; case 'user_cancelled': // Handle the case where the user canceled the purchase break; case 'pending': // Handle deferred purchases (e.g., the user will pay offline with cash) break; } } catch (error) { // Handle the error } ``` Additional request parameter: | Parameter | Presence | Description | | :--------- | :------- | :----------------------------------------------------------- | | **params** | required | an object of the [`MakePurchaseParamsInput`](https://react-native.adapty.io/interfaces/makepurchaseparamsinput) type. | :::info **Version 3.8.2+**: The `MakePurchaseParamsInput` structure has been updated. `oldSubVendorProductId` and `prorationMode` are now nested under `subscriptionUpdateParams`, and `isOfferPersonalized` is moved to the upper level. Example: ```javascript makePurchase(product, { android: { subscriptionUpdateParams: { oldSubVendorProductId: 'old_product_id', prorationMode: 'charge_prorated_price' }, isOfferPersonalized: true, obfuscatedAccountId: 'account_123', obfuscatedProfileId: 'profile_456' } }); ``` ::: 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: ```typescript 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: react-native-migration-guide-380.md --- --- title: "Migrate Adapty React Native SDK to v. 3.8" description: "Migrate to Adapty React Native 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. ## Update input type for getting placement params `GetPaywallParamsInput` has been renamed to `GetPlacementParamsInput`: ```diff showLineNumbers - type GetPaywallParamsInput = { + type GetPlacementParamsInput = { placementId: string; locale?: string; fetchPolicy?: AdaptyPlacementFetchPolicy; loadTimeoutMs?: number; } ``` ## Update fallback method The method for setting fallbacks has been updated, and the type for specifying fallback locations has been renamed: ```diff showLineNumbers - adapty.setFallbackPaywalls(paywallsLocation: Input.FallbackPaywallsLocation); + adapty.setFallback(fileLocation: Input.FileLocation); ``` ## Update paywall property access The following properties have been moved from `AdaptyPaywall` to `AdaptyPlacement`: ```diff showLineNumbers - paywall.abTestName - paywall.audienceName - paywall.revision - paywall.placementId + paywall.placement.abTestName + paywall.placement.audienceName + paywall.placement.revision + paywall.placement.id ``` --- # File: react-native-onboardings.md --- --- title: "Onboardings in React Native SDK" description: "Learn how to work with onboardings in your React Native app with Adapty SDK." displayed_sidebar: sdkreactnative --- This page contains all guides for working with onboardings in your React Native app. Choose the topic you need: - **[Get onboardings](react-native-get-onboardings)** - Retrieve onboardings from Adapty - **[Display onboardings](react-native-present-onboardings)** - Present onboardings to users - **[Handle onboarding events](react-native-handling-onboarding-events)** - Manage onboarding interactions --- # File: react-native-paywalls.md --- --- title: "Paywalls in React Native SDK" description: "Learn how to work with paywalls in your React Native app with Adapty SDK." displayed_sidebar: sdkreactnative --- This page contains all guides for working with paywalls in your React Native app. Choose the topic you need: - **[Get paywalls](react-native-get-pb-paywalls)** - Retrieve paywalls from Adapty - **[Display paywalls](react-native-present-paywalls)** - Present paywalls to users - **[Handle paywall events](react-native-handling-events-1)** - Manage paywall interactions - **[Work with paywalls offline](react-native-use-fallback-paywalls)** - Use fallback paywalls when offline - **[Localize paywalls](react-native-localizations-and-locale-codes)** - Support multiple languages - **[Implement web paywalls](react-native-web-paywall)** - Use web-based paywalls - **[Implement paywalls manually](react-native-implement-paywalls-manually)** - Build custom paywall UI --- # File: react-native-present-onboardings.md --- --- title: "Present onboardings in React Native SDK" description: "Discover how to present onboardings on React Native 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 React Native SDK](sdk-installation-reactnative.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 React Native SDK provides two ways to present onboardings: - **Standalone screen**: Modal presentation that can be dismissed by users through native platform gestures (swipe, back button). Best for optional onboardings where users should be able to skip or dismiss the content. - **Embedded component**: Embedded component gives you complete control over dismissal through your own UI and logic. Ideal for required onboardings where you want to ensure users complete the flow before proceeding. ## Present as standalone screen To display an onboarding as a standalone screen that users can dismiss, 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 `AdaptyUIError.viewAlreadyPresented` error. ::: :::note This approach is best for optional onboardings where users should have the freedom to dismiss the screen using native gestures (swipe down on iOS, back button on Android). To have more customization options, [embed it in the component hierarchy](#embed-in-component-hierarchy). ::: ```typescript showLineNumbers title="React Native (TSX)" const view = await createOnboardingView(onboarding); view.registerEventHandlers(); // handle close press, etc try { await view.present(); } catch (error) { // handle the error } ``` ## Embed in component hierarchy To embed an onboarding within your existing component tree, use the `AdaptyOnboardingView` component directly in your React Native component hierarchy. This approach gives you full control over when and how the onboarding can be dismissed. :::note This approach is ideal for required onboardings, mandatory tutorials, or any flow where you need to ensure users complete the onboarding before proceeding. You can control dismissal through your own UI elements and logic. ::: ```typescript showLineNumbers title="React Native (TSX)" ``` ## Next steps Once you've presented your onboarding, you'll want to [handle user interactions and events](react-native-handling-onboarding-events.md). Learn how to handle onboarding events to respond to user actions and track analytics. --- # File: react-native-present-paywalls-legacy.md --- --- title: "Present legacy Paywall Builder paywalls in React Native SDK" description: "Present paywalls in React Native (Legacy) 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 **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 Builder and remote config paywalls. - For presenting **New Paywall Builder paywalls**, check out [React Native - Present new Paywall Builder paywalls](react-native-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="React Native (TSX)" const view = await createPaywallView(paywall); view.registerEventHandlers(); // handle close press, etc try { await view.present(); } catch (error) { // handle the error } ``` **Next step:** - [Handle paywall events](react-native-handling-events-legacy) --- # File: react-native-present-paywalls.md --- --- title: "React Native - Present new Paywall Builder paywalls" description: "Present paywalls in React Native 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 **new Paywall Builder paywalls** only, which require SDK v3.0 or later. The process for presenting paywalls differs for paywalls designed with different versions of Paywall Builder and remote config paywalls. - For presenting **legacy Paywall Builder paywalls**, check out [React Native - Present Paywall Builder paywalls](react-native-present-paywalls-legacy). - 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="React Native (TSX)" const view = await createPaywallView(paywall); view.registerEventHandlers(); // handle close press, etc 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 title="React Native (TSX)" let customTimers = { 'CUSTOM_TIMER_NY': new Date(2025, 0, 1) } //and then you can pass it to createPaywallView as follows: 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 `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). --- # File: react-native-quickstart-identify.md --- --- title: "Identify users in React Native SDK" description: "Quickstart guide to setting up Adapty for in-app subscription management in React Native." --- :::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). ## 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. ::: ```typescript showLineNumbers try { await adapty.identify("YOUR_USER_ID"); // Unique for each user // 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 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, created anonymous profiles won't affect the dashboard [analytics](analytics-charts.md), because installs will be counted by new device IDs. However, if you want to change this behavior and count new customer user IDs instead of device IDs, go to **App settings** and set up [**Installs definition for analytics**](general#4-installs-definition-for-analytics). ::: ```typescript showLineNumbers adapty.activate("PUBLIC_SDK_KEY", { 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. }); ``` ### 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. ::: ```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 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`](react-native-check-subscription-status.md) right after the identification, or [listen for profile updates](react-native-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**](react-native-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**](react-native-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: react-native-quickstart-paywalls.md --- --- title: "Enable purchases by using paywalls in React Native SDK" description: "Learn how to present paywalls in your React Native app with Adapty SDK." slug: /react-native-quickstart-paywalls displayed_sidebar: sdkreactnative --- 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-making-purchases). | | 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. ## 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 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="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, 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 will definitely need to support opening links when users tap them. :::tip Read our guides on how to handle button [actions](react-native-handle-paywall-actions.md) and [events](react-native-handling-events-1.md). ::: ```typescript showLineNumbers title="React Native" const unsubscribe = view.registerEventHandlers({ onUrlPress(url) { Linking.openURL(url); return false; }, }); ``` ## Next steps Your paywall is ready to be displayed in the app. 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" export default function PaywallScreen() { const showPaywall = async () => { try { const paywall = await adapty.getPaywall('YOUR_PLACEMENT_ID'); if (!paywall.hasViewConfiguration) { // use your custom logic return; } const view = await createPaywallView(paywall); view.registerEventHandlers({ onUrlPress(url) { Linking.openURL(url); return false; }, }); await view.present(); } catch (error) { // handle any error that may occur during the process console.warn('Error showing paywall:', error); } }; // you can add a button to manually trigger the paywall for testing purposes return (