# 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
: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. |
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 theoptional
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. :::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).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 } } ```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: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: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:
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 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: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: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:
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.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: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::::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 (`
:::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.
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.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.:::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
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:
Specify the time interval for aggregating analytics data, to view results grouped by selected periods, such as days, weeks, months, etc. Possible values are:
Sets the basis for segmentation. Possible values are:
Specify the export file format. Available options are:
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 exampleSpecify the time interval for aggregating analytics data, to view results grouped by selected periods, such as days, weeks, months, etc. Possible values are:
Specify how values are displayed. Possible values are:
Sets the basis for segmentation. Possible values are:
Specify the export file format. Available options are:
Specify the time interval for aggregating analytics data, to view results grouped by selected periods, such as days, weeks, months, etc. Possible values are:
Possible values are:
Possible values are:
Specify the export file format. Available options are:
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 |
Specify the time interval for aggregating analytics data, to view results grouped by selected periods, such as days, weeks, months, etc. Possible values are:
Sets the basis for segmentation. Possible values are:
Specify the export file format. Available options are:
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: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. :::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: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. :::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: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. :::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: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 theoptional
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: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 |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|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.
|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. :::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: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"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).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 StateAn [`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\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:(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
Reinstallations or logging in on any user's device aren't counted as new installs.
Note that if you are not
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 implementoptional
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.
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
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: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: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 ) { } ```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:
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.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.**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`.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**(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: |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: |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:```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).
: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 |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: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: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. :::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: RecordThis 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.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.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).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)"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: react-native-sdk-migration-guides.md --- --- title: "React Native SDK Migration Guides" description: "Migration guides for Adapty React Native SDK versions." --- This page contains all migration guides for Adapty React Native SDK. Choose the version you want to migrate to for detailed instructions: - **[Migrate to v. 3.8](react-native-migration-guide-380)** - **[Migrate to v. 3.4](migration-to-react-native-sdk-34)** - **[Migrate to v. 3.3](migration-to-react-native330)** - **[Migrate to v. 3.0](migration-to-react-native-sdk-v3)** --- # File: react-native-sdk-models.md --- --- title: "React Native SDK Models" description: "Data models and types for React Native Adapty SDK." slug: /react-native-sdk-models displayed_sidebar: sdkreactnative --- ## 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 | | version | number (optional) | Version of the onboarding configuration | | payloadData | string (optional) | Additional payload data | | onboardingBuilder | [AdaptyOnboardingBuilder](#adaptyonboardingbuilder) (optional) | Builder configuration for the onboarding | ### AdaptyOnboardingBuilder | Name | Type | Description | |----|----|-----------| | url | string | URL for the onboarding builder | | lang | string | Language for the onboarding builder | ### 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 | | regionCode | string (optional) | The region code of the locale used to format the price of the product. ISO 3166 ALPHA-2 (US, DE) | | price | [AdaptyPrice](#adaptyprice) (optional) | The cost of the product in the local currency | | subscription | [AdaptySubscriptionDetails](#adaptysubscriptiondetails) (optional) | Detailed information about subscription (intro, offers, etc.) | | webPurchaseUrl | string (optional) | URL for web purchase functionality | | payloadData | string (optional) | Additional payload data | | 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. | ### 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 | ### AdaptySubscriptionDetails | Name | Type | Description | |:------------------------------|:------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | subscriptionPeriod | [AdaptySubscriptionPeriod](#adaptysubscriptionperiod) | 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 | [AdaptySubscriptionOfferId](#adaptysubscriptionofferid) (optional) | A subscription offer if available for the auto-renewable subscription | | ios | object (optional) | iOS-specific properties | | ios.subscriptionGroupIdentifier | 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. | ### AdaptySubscriptionPeriod | 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` | ### AdaptySubscriptionOfferId | Name | Type | Description | |----|----|-----------| | id | string (optional) | Identifier for promotional or win_back offers | | type | string | Type of offer. Possible values: `introductory`, `promotional`, `win_back` | ### AdaptyDiscountPhase | 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 | [AdaptySubscriptionPeriod](#adaptysubscriptionperiod) | 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 | | products | array of [ProductReference](#productreference) | Array of initial products info | | id | string | An identifier of a paywall, configured in Adapty Dashboard | | version | number (optional) | Version of the paywall configuration | | webPurchaseUrl | string (optional) | URL for web purchase functionality | | payloadData | string (optional) | Additional payload data | | paywallBuilder | [AdaptyPaywallBuilder](#adaptypaywallbuilder) (optional) | Builder configuration for the paywall | ### AdaptyPaywallBuilder | Name | Type | Description | |----|----|-----------| | id | string | ID for the paywall builder | | lang | string | Language for the paywall builder | ### 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\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. ```typescript showLineNumbers try { await adapty.updateProfile({ codableCustomAttributes: { key_1: 'value_1', key_2: 2, }, }); } catch (error) { // handle `AdaptyError` } ``` To remove existing key, use `.withRemoved(customAttributeForKey:)` method: ```typescript showLineNumbers try { // to remove a key, pass null as its value await adapty.updateProfile({ codableCustomAttributes: { key_1: null, key_2: null, }, }); } catch (error) { // handle `AdaptyError` } ``` 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: react-native-test.md --- --- title: "Test & release in React Native SDK" description: "Learn how to test and release your React Native app with Adapty SDK." slug: /react-native-test displayed_sidebar: sdkreactnative --- If you've already implemented the Adapty SDK in your React Native 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: react-native-troubleshoot-paywall-builder.md --- --- title: "Troubleshoot Paywall Builder in React Native SDK" description: "Troubleshoot Paywall Builder in React Native SDK" --- This guide helps you resolve common issues when using paywalls designed in the Adapty Paywall Builder in the React Native 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](react-native-sdk-migration-guides) if needed. Many issues are resolved in newer SDK versions. --- # File: react-native-troubleshoot-purchases.md --- --- title: "Troubleshoot purchases in React Native SDK" description: "Troubleshoot purchases in React Native SDK" --- This guide helps you resolve common issues when implementing purchases manually in the React Native 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-react-native) 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](react-native-sdk-migration-guides) if needed. Many issues are resolved in newer SDK versions. --- # File: react-native-use-fallback-paywalls.md --- --- title: "React Native - Use fallback paywalls" description: "Use fallback paywalls in React Native apps with Adapty for stable revenue." --- Follow the instructions below to use the fallback paywalls in your mobile app code. ### For Android 1. Place the fallback file you [downloaded in the Adapty Dashboard](fallback-paywalls#download-fallback-paywalls-as-a-file-in-the-adapty-dashboard) to a directory on the native layer. There are 2 correct directories to put the file: `android/app/src/main/assets/` or `android/app/src/main/res/raw/`. Please keep in mind that the `res/raw` folder has a special file naming convention (start with a letter, no capital letters, no special characters except for the underscore, and no spaces in the names). 1. **For android/app/src/main/assets/**: Pass the file path relatively to the `assets` directory, for example: - `{ relativeAssetPath: 'android_fallback.json' }` if you placed the file to the root of `assets` itself - `{ relativeAssetPath: '| Option | Description | | ------- | ------------------------------------------------------------ | | Opt-out | (default) If Adapty doesn't know the user's consent status, it assumes consent **was given** and Refund Saver **will share** refund-related data with Apple. | | Opt-in | If Adapty doesn't know the user's consent status, it assumes consent **was not given** and Refund Saver **won’t share** any data with Apple. This is Apple’s recommended approach. | ## Update user consent in the SDK To tell Adapty whether a specific user has given consent, use the `updateCollectingRefundDataConsent` method:
For iOS, StoreKit 1: an [SKPaymentTransaction](https://developer.apple.com/documentation/storekit/skpaymenttransaction) object.
For iOS, StoreKit 2: [Transaction](https://developer.apple.com/documentation/storekit/transaction) object.
For Android: String identifier (purchase.getOrderId of the purchase, where the purchase is an instance of the billing library [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) class.
| | variationId | required | The string identifier of the variation. You can get it using `variationId` property of the [AdaptyPaywall](sdk-models#adaptypaywall) object. |For iOS, StoreKit 1: an [SKPaymentTransaction](https://developer.apple.com/documentation/storekit/skpaymenttransaction) object.
For iOS, StoreKit 2: [Transaction](https://developer.apple.com/documentation/storekit/transaction) object.
For Android: String identifier (purchase.getOrderId of the purchase, where the purchase is an instance of the billing library [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) class.
| | variationId | required | The string identifier of the variation. You can get it using `variationId` property of the [AdaptyPaywall](sdk-models#adaptypaywall) object. |For StoreKit 1: an [SKPaymentTransaction](https://developer.apple.com/documentation/storekit/skpaymenttransaction) object.
For StoreKit 2: [Transaction](https://developer.apple.com/documentation/storekit/transaction) object.
|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: results-and-metrics.md --- --- title: "A/B test results and metrics" description: "Analyze results and key metrics in Adapty to improve your app’s subscription performance and user engagement." --- Discover important data and insights from our [A/B tests](ab-tests), comparing different paywalls and onboardings to see how they affect user behavior, engagement, and conversion rates. By looking at the metrics and results here, you can make smart choices and improve your app's performance. Dive into the data to find actionable insights and enhance your app's success. ### A/B test results Here are three metrics that Adapty provides for A/B test results: **Revenue**: This metric shows the total amount of money generated in USD from purchases and renewals, minus any refunds given to users. It includes both the initial purchase and any follow-up subscription renewals. Revenue helps you understand how each A/B test variant is performing financially and figure out which one brings in the most money. Learn more about [paywall](https://adapty.io/docs/paywall-metrics) and [onboarding](https://adapty.io/docs/onboarding-metrics) metrics. **Probability to be best**: Adapty utilizes a robust mathematical analysis framework to analyze A/B test results and provides a metric called Probability to be best. This metric assesses the likelihood that a particular variant is the best-performing option (in terms of its long-term revenue) among all the variants tested. The metric is expressed as a percentage value ranging from 1% to 100%. For detailed information on how Adapty calculates this metric, please refer to the [documentation.](maths-behind-it)The best performing option, determined by Revenue per 1K user, is highlighted in green and automatically selected as the default choice. **Revenue per 1K users**: The revenue per 1K users metric calculates the average revenue generated per 1,000 users for each A/B test variant. This metric helps you understand the revenue efficiency of your variants, regardless of the total number of users. It allows you to compare the performance of different variants on a standardized scale and make informed decisions based on revenue generation efficiency. **Prediction intervals for revenue 1K users**: The revenue per 1K users metric also includes prediction intervals. These prediction intervals represent the range within which the true revenue per 1,000 users for a given variant is predicted to fall based on the available data and statistical analysis. In the context of A/B testing, when analyzing the revenue generated by different variants, we calculate the average revenue per 1,000 users for each variant. Since revenue can vary among users, the prediction intervals provide a clear indication of the plausible values for the revenue per 1,000 users, taking into account the variability and uncertainty associated with the prediction process. By incorporating prediction intervals into the revenue per 1K users metric, Adapty enables you to assess the revenue efficiency of your A/B test variants while considering the range of potential revenue outcomes. This information helps you make data-driven decisions and optimize your subscription strategy effectively, by taking into account the uncertainty in the prediction process and the plausible values for revenue per 1,000 users. By analyzing these metrics provided by Adapty, you can gain insights into the financial performance, statistical significance, and revenue efficiency of your A/B test variants, enabling you to make data-driven decisions and optimize your subscription strategy effectively. ## A/B test metrics Adapty provides a comprehensive set of metrics to help you effectively measure the performance of your A/B test conducted on your paywall or onboarding variations. These metrics are continuously updated in real-time, except for views, which are updated periodically. Understanding these metrics will help you assess the effectiveness of different variations and make data-driven decisions to optimize your paywall or onboarding strategy. A/B test metrics are available on the A/B test list, where you can gain an overview of the performance of all your A/B tests. This comprehensive view offers aggregated metrics for each test variation, enabling you to compare their performance and identify significant differences. For a more detailed analysis of each A/B test, you can access the A/B Test detail metrics. This section provides in-depth metrics specific to the selected A/B test, allowing you to delve into the performance of individual variations. All metrics, except for views, are attributed to the product within the paywall or onboarding. ## 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. ### 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 or onboarding 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, attribution, country, paywall, paywall state, paywall group, onboarding, 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 or onboarding analytics in [this documentation.](controls-filters-grouping-compare-proceeds) ## Single metrics chart One of the key components of the paywall or onboarding metrics page is the chart section, which visually represents the selected metrics and facilitates easy analysis. The chart section on the A/B test 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. ## A/B test summary Next to the single metrics chart, the A/B test details summary section is displayed, which includes information about the state, duration, placements, and other related details about the A/B test. ## Metrics definitions Here are the key metrics that are available for the A/B tests: ### Revenue Revenue represents the total amount of money generated in USD from purchases and renewals resulting from the A/B test. It includes the initial purchase and subsequent subscription renewals. The revenue metric is calculated before deducting the App Store or Play Store commission. Learn more about [paywall](https://docs.adapty.io/docs/paywall-metrics#revenue) and [onboarding](https://docs.adapty.io/docs/onboarding-metrics#revenue) revenue metrics. ### CR to purchases The conversion rate to purchases measures the effectiveness of your A/B test in converting views into actual purchases. It is calculated by dividing the number of purchases by the number of views. For example, if you had 10 purchases and 100 views, the conversion rate to purchases would be 10%. ### CR trials The conversion rate (CR) to trials is the number of trials started from A/B test divided by the number of views. Conversion rate to trials measures the effectiveness of your A/B test in converting views into trial activations. It is calculated by dividing the number of trials started by the number of views. ### Purchases The purchases metric represents the total number of transactions made within the paywall or onboarding resulting from the A/B test. It includes the following types of purchases: - New purchases made. - Trial conversions of trials that were activated. - Downgrades, upgrades, and cross-grades of subscriptions. - Subscription restores (e.g. when a subscription is expired without auto-renewal and is subsequently restored). Please note that renewals are not included in the purchases metric. ### Trials The trials metric indicates the total number of activated trials resulting from the A/B test. ### Trials cancelled The trials canceled metric represents the number of trials in which auto-renewal has been switched off. This occurs when users manually unsubscribe from the trial. ### Refunds Refunds for the A/B test represent the number of refunded purchases and subscriptions specifically related to the tested variations. ### Views Views are the number of views of the paywalls or onboardings that the A/B test consits of. If the user visits two times, this will be counted as two visits. ### Unique views Unique views are the number of unique views of the paywall or onboarding. If the user visits it two times, this will be counted as one unique view. ### Probability to be the best The Probability to be the best metric quantifies the likelihood that a specific variant within an A/B test is the top-performing option among all the tested paywalls or onboardings. It provides a numerical probability indicating the relative performance of each paywall or onboarding. The metric is expressed as a percentage value ranging from 1% to 100%. ### ARPU (Average revenue per user) For onboarding A/B tests only. Measures the average revenue generated from each user over a specific period. It is calculated by dividing total revenue by the number of unique users. ### ARPPU (Average revenue per paying user) ARPPU stands for Average Revenue Per Paying User resulting from the A/B test. It is calculated as the total revenue divided by the number of unique paying users. For example, if you have generated $15,000 in revenue from 1,000 paying users, the ARPPU would be $15. ### ARPAS (Average revenue per active subscriber) ARPAS is a metric that allows you to measure the average revenue generated per active subscriber from running the A/B test. 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 you have 1,000 subscribers, the ARPAS would be $5. This metric helps assess the average monetization potential per subscriber. ### Proceeds The proceeds metric for the A/B test 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 specifically associated with the variations tested in the A/B test, contributing directly 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) ### Unique subscribers The unique subscribers metric represents the count of distinct individuals who has subscribed or activated a trial through the variations in the A/B test. It considers each subscriber only once, irrespective of the number of subscriptions or trials they initiate. ### Unique paid subscribers The unique paid subscribers metric represents the number of unique individuals who have successfully completed a purchase and become paying subscribers through the variations in the A/B test. ### Refund rate The refund rate for the A/B test is calculated by dividing the number of refunds specifically associated with the variations in the test by the number of first-time purchases (renewals are excluded). For instance, if there are 5 refunds and 1000 first-time purchases, the refund rate would be 0.5%. ### Unique CR purchases The unique conversion rate to purchases for the A/B test is calculated by dividing the number of purchases specifically associated with the variations in the test 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%. ### Unique CR trials The unique conversion rate to trials for the A/B test is calculated by dividing the number of trials started specifically associated with the variations in the test 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%. ### Completions & unique completions For onboarding A/B tests only. Completions count the number of times users complete your onboarding through the variations in the A/B test, 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 A/B tests only. The unique completion number divided by the unique view number. This metric helps you understand how people engage with onboarding through the variations in the A/B test and make changes if you notice that people ignore it. --- # File: revenue.md --- --- title: "Revenue" description: "Track and analyze your app’s revenue using Adapty’s subscription insights." --- The revenue chart displays the total revenue earned from both subscriptions and one-time purchases, minus the revenue that was refunded later. The Revenue chart is an essential tool to monitor the financial performance of the app. ### Calculation The Adapty revenue chart calculates the total revenue generated by the app minus the refunded revenue. This includes both new revenue, which comes from a customer's first paid transaction, such as new paid subscriptions, initial non-consumable and consumable purchases, and trial-to-paid conversions, as well as renewal revenue, which comes from subsequent paid transactions such as paid subscription renewals, promotional offers for existing subscribers, additional non-renewing subscription purchases, and more. The calculation is done before deducting the store's fees and taxes. Revenue from each transaction is attributed to the period in which the transaction occurred, which can lead to period-over-period fluctuations based on the mix of subscription durations being started or renewed. It's important to note that refunds are treated as negative revenue and attributed to the day they occurred on (not to the day the subscription began). Revenue = Total amount billed to customers - Revenue from refunded purchases/subscriptions. For example, there were 5 monthly $10 subs, 1 yearly $100 sub, and 10 one-time $50 purchases today, revenue = 5_$10 + 1_$100 + 10\*$50 = $650 After calculating the total revenue earned from in-app purchases, Adapty's Revenue chart provides an estimate of the expected proceeds by deducting the store's commission fee and taxes. The commission fee is calculated based on the gross revenue, while taxes are deducted before the commission fee is applied. This ensures that the displayed proceeds reflect the net revenue received by the app after accounting for both the store's commission and applicable taxes. To learn more about how Adapty calculates commission fees, taxes, and estimates the expected proceeds, refer to the relevant [documentation.](controls-filters-grouping-compare-proceeds#store-commission-and-taxes) ### 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) ### Revenue chart usage The Revenue chart is a valuable tool to track the financial performance of the app. By analyzing the chart's data, you can gain insight into the growth trajectory of your app over time. One useful approach is to switch to a monthly resolution and observe the last 12 months of revenue to assess the app's overall trend. It is also helpful to analyze the mix of new and renewal revenue to identify where growth is coming from. You can further segment the data by different dimensions, such as product or user type, to gain deeper insights into the app's financial performance. Overall, the Revenue chart is an essential metric for understanding the financial health of an app and optimizing its revenue strategy. ### Similar metrics In addition to Revenue, Adapty also provides metrics for other revenue-related events, such as MRR, ARR, ARPU, and ARPPU. To learn more about these revenue-related metrics, please refer to the following documentation guides: - [MRR](mrr) - [ARR](arr) - [ARPU](arpu) - [ARPPU](arppu) --- # File: run_stop_ab_tests.md --- --- title: "Run and stop A/B test" description: "Learn how to run and stop A/B tests in Adapty to optimize subscription conversions." --- Running an A/B test in Adapty means assigning it to a placement so it can start showing paywalls and onboardings to users. ## How to run the A/B test 1. Go to the [A/B tests](ab-tests) section from the Adapty main menu. 2. Make sure you're viewing the correct list — **Regular**, **Onboardings** and **Crossplacement** A/B tests are shown in separate tabs that you can switch between. 3. Switch to the **Drafts** tab. Only draft tests can be started. 4. Click the **Run A/B test** button next to the test you want to launch. 5. The **Edit A/B test** window will open so you can review and make any final changes before launching the test. If something important is missing — like the placement or audience — you’ll be able to add it now. Keep in mind that once the test is live, you won’t be able to make any edits. To apply changes later, you’ll need to stop the test and create a new one. 6. Once everything looks good, click **Run A/B test** to start. After launching the test, you can track its progress and view performance data on the [A/B test metrics](results-and-metrics) page. This helps you spot the best-performing variation and make smarter decisions. For more details on how Adapty calculates these metrics, check out Math behind the A/B tests. ## How to stop the A/B test Stopping an A/B test means you're ready to end it and review the results. This step is key for wrapping up the test and deciding what to show users next. 1. Open the [A/B tests](https://app.adapty.io/ab-tests) section and go to the **Live** tab. 2. Click the three-dot menu next to the test you want to stop, then choose **Stop A/B test**. 3. In the **Stop the A/B test** window, decide what should happen after the test ends. You have three options: | Option | Description | |----------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Display one of the tested paywalls/onboardings | Choose the winning paywall or onboarding based on test results like revenue, probability to be best (**P2BB**), and revenue per 1K users. This paywall or onboarding will be shown for the selected placement and audience. | | Select paywalls/onboardings that don’t participate in A/B test | In this case, you can choose any paywall or onboarding that isn’t part of the current A/B test. This is helpful when the test results show that none of the tested items performed well, and you want to continue with a more effective one instead. | | Don’t show any specific paywall/onboarding | For the selected placement and audience, no specific paywall or onboarding will be selected after the A/B test ends. Instead, the next available paywall or onboarding based on audience priority will be shown. This is a good choice if you’d rather let your existing setup decide which paywall or onboarding to display, without manually selecting one. | 4. Click the **Stop and complete this A/B test** button. Once the A/B test is finished, it will no longer be active, and the paywalls or onboardings from it will no longer be displayed to users. However, you can still access the A/B test results and metrics on the [A/B test metrics page](results-and-metrics#metrics-controls) to analyze the data collected during the test. :::note Stopping an A/B test is irreversible, and the test cannot be restarted once it has been stopped. Ensure that you have gathered sufficient data and insights before making the decision to stop an A/B test. ::: --- # File: s3-exports.md --- --- title: "Amazon S3" description: "Export subscription data to S3 for advanced analytics and reporting." --- Adapty's integration with Amazon S3 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 Amazon S3 bucket as .csv files. To set up this integration, you will need to follow a few simple steps in the AWS Console and Adapty dashboard. :::note Schedule Adapty sends your data 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 Amazon S3 integration To start receiving data, you'll need the following credentials: 1. Access key ID 2. Secret access key 3. S3 bucket name 4. Folder name inside the S3 bucket :::note Nested directories You can specify nested directories in the Amazon S3 bucket name field, e.g. adapty-events/com.sample-app ::: To integrate Amazon S3 go to [**Integrations** -> **Amazon S3**](https://app.adapty.io/integrations/s3), turn on a toggle from off to on, and fill out fields. First of all set credentials to build a connection between Amazon S3 and Adapty profiles. In the Adapty Dashboard, the following fields are needed to set up the connection: | Field | Description | | :--------------------------- | :----------------------------------------------------------- | | **Access Key ID** | A unique identifier that is used to authenticate a user or application's access to an AWS service. Find this ID in the downloaded [csv file](s3-exports#how-to-create-amazon-s3-credentials) . | | **Secret Access Key** | A private key that is used in conjunction with the Access Key ID to authenticate a user or application's access to an AWS service. Find this Key in the downloaded [csv file](s3-exports#how-to-create-amazon-s3-credentials) . | | **S3 Bucket Name** | A globally unique name that identifies a specific S3 bucket within the AWS cloud. S3 buckets are a simple storage service that allows users to store and retrieve data objects, such as files and images, in the cloud. | | **Folder Inside the Bucker** | The name of the folder that you want to have inside the selected S3 bucket. Please note that S3 simulates folders using object key prefixes, which are essentially folder names. | ## How to create Amazon S3 credentials This guide will help you create the necessary credentials in your AWS Console. ### 1\. Create Access Policy First, navigate to the [IAM Policy Dashboard](https://us-east-1.console.aws.amazon.com/iamv2/home?region=us-east-1#/policies) in your AWS Console and select the option to **Create Policy**. In the Policy editor, paste the following JSON and change `adapty-s3-integration-test` to your bucket name: ```json showLineNumbers title="Json" { "Version": "2012-10-17", "Statement": [ { "Sid": "AllowListObjectsInBucket", "Effect": "Allow", "Action": "s3:ListBucket", "Resource": "arn:aws:s3:::adapty-s3-integration-test" }, { "Sid": "AllowAllObjectActions", "Effect": "Allow", "Action": "s3:*Object", "Resource": [ "arn:aws:s3:::adapty-s3-integration-test/*", "arn:aws:s3:::adapty-s3-integration-test" ] }, { "Sid": "AllowBucketLocation", "Effect": "Allow", "Action": "s3:GetBucketLocation", "Resource": "arn:aws:s3:::adapty-s3-integration-test" } ] } ``` After completing the policy configuration, you may choose to add tags (optional) and then click **Next** to proceed to the final step. In this step, you will name your policy and simply click on the **Create policy** button to finalize the creation process. ### 2\. Create IAM user To enable Adapty to upload raw data reports to your bucket, you will need to provide them with the Access Key ID and Secret Access Key for a user who has write access to the specific bucket. To proceed with that, navigate to the IAM Console and select the [Users section](https://console.aws.amazon.com/iamv2/home#/users). From there, click on the **Add users** button. Give the user a name, choose **Access key – Programmatic access**, and proceed to permissions. For the next step, please select the **Add user to group** option and then click the **Create group** button. Next, you need to assign a name to your User Group and select the policy that was previously created by you. Once you have selected the policy, click on the **Create group** button to complete the process. After successfully creating the group, please **select it** and proceed to the next step. Since this is the final step for this section, you may proceed by simply clicking on the **Create User** button. Lastly, you can either **download the credentials in .csv** format or alternatively, copy and paste the credentials directly from the dashboard. ## Manual data export In addition to the automatic event data export to Amazon S3, 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 S3 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 AWS S3 integration, Adapty provides a 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** | Lower cased 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_county** | 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: sample-apps.md --- --- title: "Sample apps" description: "" --- To help you get started with Adapty SDK, we've prepared sample apps that demonstrate how to integrate and use its key features. These apps provide ready-to-use implementations of paywalls, purchases, and analytics tracking. ### Why use sample apps? - **Quick Integration:** See how Adapty SDK works in a real app. - **Best Practices:** Follow recommended implementation patterns. - **Debugging & Testing:** Use the sample apps to troubleshoot and experiment before integrating Adapty into your own project. ### Available 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: sdk-installation-android.md --- --- title: "Install & configure Android SDK" description: "Step-by-step guide on installing Adapty SDK on Android for subscription-based apps." --- Adapty SDK includes two key modules for seamless integration into your mobile app: - **Core Adapty**: This essential SDK is required for Adapty to function properly in your app. - **AdaptyUI**: This module is needed if you use the [Adapty Paywall Builder](adapty-paywall-builder), a user-friendly, no-code tool for easily creating cross-platform paywalls. :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample app](https://github.com/adaptyteam/AdaptySDK-Android), which demonstrates the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: :::info Adapty supports Google Play Billing Library up to 7.x. Support for [Billing Library 8.0.0 (released 30 June, 2025)](https://developer.android.com/google/play/billing/release-notes#8-0-0) is planned. ::: ## Install Adapty SDK Choose your dependency setup method: - Standard Gradle: Add dependencies to your **module-level** `build.gradle` - If your project uses `.gradle.kts` files, add dependencies to your module-level `build.gradle.kts` - If you use version catalogs, add dependencies to your `libs.versions.toml` file and then, reference it in `build.gradle.kts` [](https://github.com/adaptyteam/AdaptySDK-Android/releases)This is the product ID from the vendor's system (App Store, Google Play, Stripe, etc.) that unlocked the access level.
If an access level was granted without a transaction, one of the following values will be returned:
This is the product ID from the vendor's system (App Store, Google Play, Stripe, etc.) that unlocked the access level.
If an access level was granted without a transaction, one of the following values will be returned:
This is the product ID from the vendor's system (App Store, Google Play, Stripe, etc.) that unlocked the access level.
If an access level was granted without a transaction, one of the following values will be returned:
For subscriptions, this ID links the original transaction in the chain of renewals. Later transactions are linked as renewals.
If there's no renewal, store_original_transaction_id matches store_transaction_id.
| | offer | Object | Yes | No | The [Offer](server-side-api-objects#offer) object. Can be `null` if the customer has no access levels. | | environment | String | No | No | Environment for the transaction that granted access. Options: `Sandbox`, `Production`. | | starts_at | ISO 8601 date | Yes | Yes | The date time when the access level becomes active. Could be in the future. | | purchased_at | ISO 8601 date | Yes | No | The datetime of the most recent purchase for the access level. | | originally_purchased_at | ISO 8601 date | Yes | No | For subscriptions, this is the date and time of the very first (original) purchase in the chain, tied to `store_original_transaction_id`. | | expires_at | ISO 8601 date | Yes | Yes | The datetime when the access level expires. Might be in the past, or `null` for lifetime access. | | renewal_cancelled_at | ISO 8601 date | Yes | Yes | The datetime when auto-renewal was turned off for a subscription. The subscription can still be active; it just won't auto-renew. Set to `null` if the user reactivates the subscription. | | billing_issue_detected_at | ISO 8601 date | Yes | Yes | The datetime when a billing issue was found (like a failed card charge). The subscription might still be active. This is cleared if the payment goes through later. | | is_in_grace_period | Boolean | Yes | No | Shows whether the subscription is in a [grace period](https://developer.apple.com/news/?id=09122019c) (only for auto-renewable subscriptions). | | cancellation_reason | String | Yes | Yes | Reason for cancellation, with options like: `voluntarily_cancelled`, `billing_error`, `price_increase`, `product_was_not_available`, `refund`, `upgraded`, `unknown`. | :::note Although the SDK includes the `is_active` parameter to check if a subscription is active, the server-side API does not provide this parameter. However, you can determine subscription status at any time by checking whether the current date falls between the `starts_at` and `expires_at` parameters. ::: --- ## Installation Meta Information about installation of the app on a specific device. You can do the following action via Adapty server-side API: - [Create a profile with spesific installation meta](ss-create-profile) - [Update user's installation meta](ss-update-profile) | Parameter | Type | Required | Nullable | Description | | :----------------- | :----- | -------- | -------- | :----------------------------------------------------------- | | device_id | String | Yes | No | The device identifier is generated on the client side. | | device | String | No | Yes | The end-user-visible device model name. | | locale | String | No | Yes | The locale used by the end user. | | os | String | No | Yes | The operating system used by the end user. | | platform | String | No | Yes | The device platform used by the end user. | | timezone | String | No | Yes | The timezone of the end user. | | user_agent | String | No | Yes | Details about the end user environment: device, operating system, and browser information of the end user interacting with your application. | | idfa | String | No | Yes | The Identifier for Advertisers, assigned by Apple to a user's device. | | idfv | String | No | Yes | The Identifier for Vendors (IDFV) is a code assigned to all apps by one developer and is shared across all apps by that developer on your device. | | advertising_id | String | No | Yes | The Advertising ID is a unique identifier offered by the Android Operating System that advertisers might use to uniquely identify you. | | android_id | String | No | Yes | 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). | | android_app_set_id | String | No | Yes | 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. | --- ## Non Subscription Info about non-subscription purchases. These can be one-time \(consumable\) products, unlocks \(like new map unlock in the game\), etc. You can do the following action via Adapty server-side API: - [Check user's current non-subscriptions](ss-get-profile) by retrieving their profile details | Parameter | Type | Required | Nullable | Description | | :---------------------------- | :------------ | -------- | -------- | :----------------------------------------------------------- | | purchase_id | String | Yes | No | 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 | Yes | No | Store where the product was purchased. Possible values are: **app_store**, **play_store**, **stripe**, name of your [custom store](custom-store). | | store_product_id | String | Yes | No | Identifier of the product in the app store (App Store/Google Play/Stripe, etc.) that unlocked this access level. | | store_base_plan_id | String | Yes | Yes | [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. | | store_transaction_id | String | Yes | No | The ID of the transaction in the app store (App Store/Google Play/Stripe, etc.). | | store_original_transaction_id | String | Yes | No |In case of prolonged subscriptions, a chain of subscriptions is generated. The original transaction i the very first transaction in this chain and the chain is linked by it. Other transactions in the chain are prolongations.
If no prolongation, `store_original_transaction_id` will coincide with `store_transaction_id`.
| | purchased_at | ISO 8601 date | Yes | No | The datetime when the access level was purchased the latest time. | | environment | String | No | No | Environment of the transaction that provided the access level. Possible values: `Sandbox`, `Production.` | | is_refund | Boolean | Yes | No | Indicates if the product has been refunded. | | is_consumable | Boolean | Yes | No | Indicates whether the product is consumable. | --- ## One-Time Purchase | Parameter | Type | Required | Nullable | Description | | :---------------------------- | :------------ | -------- | -------- | :----------------------------------------------------------- | | purchase_type | String | Yes | No | The type of product purchased. Possible value: `one_time_purchase`. | | store | String | Yes | No | Store where the product was bought. Possible values: `app_store`, `play_store`, `stripe`, or the Store ID of your [custom store](custom-store). | | environment | String | No | No | Transaction environment that provided the access level. Options: `Sandbox`, `Production`. `Production` is used by default. | | store_product_id | String | Yes | No | The product ID in the app store (App Store, Google Play, Stripe, etc.) that unlocked this access level. | | store_transaction_id | String | Yes | No | Transaction ID in the app store (App Store, Google Play, Stripe, etc.). | | store_original_transaction_id | String | Yes | No |For recurring subscriptions, this is the original transaction ID that links the chain of renewals. The original transaction is the first in the chain; later transactions are renewals.
If there's no renewal, `store_original_transaction_id` matches `store_transaction_id`.
| | offer | Object | No | Yes | The offer used for the purchase as an [Offer](server-side-api-objects#offer) object. | | is_family_shared | Boolean | No | No | 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. `false` is used by default. | | price | Object | Yes | No | Price of the one-time purchase as a [Price](server-side-api-objects#price) object. An initial subscription purchase with zero cost is a free trial; a renewal with zero cost is a free renewal. | | purchased_at | ISO 8601 date | Yes | No | The datetime when the access level was last purchased. | | refunded_at | ISO 8601 date | No | No | If refunded, shows the datetime of the refund. | | cancellation_reason | String | No | No | Possible reasons for cancellation: `voluntarily_cancelled`, `billing_error`, `price_increase`, `product_was_not_available`, `refund`, `cancelled_by_developer`, `new_subscription`, `unknown`. | | variation_id | String | No | No | The variation ID used to trace purchases to the specific paywall they were made from. | --- ## Offer Information on the applied offer. The Offer object is a part of the [Subscription](server-side-api-objects#subscription), and [Access level](server-side-api-objects#access-level) objects. You can do the following actions with offers via Adapty server-side API: - [Apply offer](ss-set-transaction) when setting a transaction to your user | Parameter | Type | Required | Nullable | Description | | -- | ------ | -------- | -------- | ------------------------------------------------------------ | | category | String | Yes | No | The category of the applied offer. Options are: **introductory**, **promotional**, **offer_code**, **win_back**. | | type | String | Yes | No | The type of active offer. Options are: **free_trial**, **pay_as_you_go**, **pay_up_front**, and **unknown**. If this isn't null, it means the offer was applied in the current subscription period. | | id | String | No | Yes | The ID of the applied offer. | ## Price Information about the cost of your product in local currency. The Price object is a part of the [Subscription](server-side-api-objects#subscription) and Purchase objects. You can do the following actions with product price via Adapty server-side API: - [Set transaction to your user](ss-set-transaction) and specify its price | Parameter | Type | Required | Nullable | Description | | --------- | ------ | -------- | -------- | ----------------------------------------- | | country | String | Yes | No | The country where the price applies. | | currency | String | Yes | No | The currency used for the price. | | value | Float | Yes | No | The product's cost in the local currency. | --- ## Profile Info about the [customer and their subscription](server-side-api-objects#profile) You can do the following actions with user profiles via Adapty server-side API: - [Retrieve/get the end-user's profile](ss-get-profile) with their access levels, subscriptions, non-subscriptions, etc. - [Create a new end-user profile](ss-create-profile) - [Update your end-user profile](ss-update-profile) - [Delete your end-user](ss-delete-profile) | Parameter | Type | Nullable | Description | | ----------------- | ---------- | ------------------ | ------------------------------------------------------------ | | app_id | String | :heavy_minus_sign: | The internal ID of your app. You can see in the the Adapty Dashboard: [App Settings -> General tab](https://app.adapty.io/settings/general). | | profile_id | UUID | :heavy_minus_sign: | Adapty profile ID. You can see it in the **Adapty ID** field on the Adapty Dashboard -> [Profiles](https://app.adapty.io/profiles/users) -> specific profile page. | | customer_user_id | String | :heavy_plus_sign: | The ID of your user in your system. You can see it in the **Customer user ID** field on the Adapty Dashboard -> [Profiles](https://app.adapty.io/profiles/users) -> specific profile page. It will work only if you [identify the users](identifying-users) in your mobile app code via Adapty SDK. | | total_revenue_usd | Float | :heavy_minus_sign: | A float value representing the total revenue in USD earned in the profile. | | segment_hash | String | :heavy_minus_sign: | Internal parameter. | | timestamp | Integer | :heavy_minus_sign: | Response time in milliseconds, needs for resolve a race condition. | | custom_attributes | Array | :heavy_minus_sign: |A maximum of 30 custom attributes to the profile are allowed to be set. If you provide the `custom_attributes` array, you must provide at least one attribute key.
**Key:** The key must be a string with no more than 30 characters. Only letters, numbers, dashes, points, and underscores allowed
**Value:** The attribute value must be no more than 30 characters. Only strings and floats are allowed as values, booleans will be converted to floats. Send an empty value or null to delete the attribute.
| | access_levels | Array | :heavy_plus_sign: | Array of [Access level](https://adapty.io/docs/server-side-api-objects#customeraccesslevel) objects. Can be null if the customer has no access levels. | | subscriptions | Array | :heavy_plus_sign: | Array of [Subscription](https://adapty.io/docs/server-side-api-objects#subscription) objects. Can be null if the customer has no subscriptions. | | non_subscriptions | Array | :heavy_plus_sign: | Array of [Non-Subscription](https://adapty.io/docs/server-side-api-objects#non-subscription) objects. Can be null if the customer has no purchases. | ## Product This object contains details about a product in Adapty. | Name | Type | Required | Description | | ------------------------------ | ------- | -------- | ------------------------------------------------------------ | | title | String | No | **Product name** from the [**Products**](https://app.adapty.io/products) section in the Adapty Dashboard. | | is_consumable | Boolean | Yes | Indicates whether the product is consumable. | | adapty_product_id | UUID | No | Internal product ID as used in Adapty. | | vendor_product_id | String | Yes | The product ID in app stores. | | introductory_offer_eligibility | Boolean | No | Specifies if the user is eligible for an iOS introductory offer. | | promotional_offer_eligibility | Boolean | No | Specifies if the user is eligible for a promotional offer. | | base_plan_id | String | No | [Base plan ID](https://support.google.com/googleplay/android-developer/answer/12154973) for Google Play or [price ID](https://docs.stripe.com/products-prices/how-products-and-prices-work#what-is-a-price) for Stripe. | | offer | JSON | No | An [Offer](web-api-objects#offer-object) object as a JSON. | ```json showLineNumbers { "title": "Monthly Subscription w/o Trial", "is_consumable": true, "adapty_product_id": "InternalProductId", "vendor_product_id": "onemonth_no_trial", "introductory_offer_eligibility": false, "promotional_offer_eligibility": true, "base_plan_id": "B1", "offer": { "category": "promotional", "type": "pay_up_front", "id": "StoreOfferId" } } ``` ## RemoteConfig This object contains information about a [remote config](customize-paywall-with-remote-config) for a paywall. ```json showLineNumbers { "lang": "en", "data": "{\"bodyItems\":[{\"spacerValue\":{\"height\":20,\"style\":{\"type\":\"emptySpace\"}},\"type\":\"spacer\"},{\"mediaValue\":{\"ratio\":\"1:1\",\"source\":{\"fileType\":\"image\",\"reference\":{\"en\":\"bundle/images/new1.png\"}},\"widthStyle\":\"full\"},\"type\":\"media\"},{\"titleValue\":{\"alignment\":\"center\",\"subtitleConfig\":{\"fontSize\":17,\"text\":\"\",\"color\":\"#FFFFFF\"},\"titleConfig\":{\"fontSize\":22,\"text\":\"\"}},\"type\":\"title\"},{\"productListValue\":{\"items\":[{\"productId\":\"exampleapp.oneWeek\",\"promoText\":\"paywall.promo-1.title\",\"backgroundColor\":\"#0B867D\"},{\"discountRate\":80,\"productId\":\"exampleapp.oneYear\",\"promoText\":\"paywall.promo-2.title\",\"backgroundColor\":\"#0B867D\"}],\"layout\":\"vertical\"},\"type\":\"productList\"}],\"defaultProductId\":\"exampleapp.oneWeek\",\"footer\":{\"singleProductValue\":{\"customTitles\":{\"exampleapp.oneWeek\":\"Subscribe\",\"exampleapp.oneYear\":\"Subscribe\"},\"productId\":\"exampleapp.oneWeek\"},\"type\":\"singleProduct\"},\"id\":\"exampleapp\",\"isFullScreen\":true,\"settings\":{\"backgroundColor\":\"#000000\",\"closeButtonAlignment\":\"left\",\"closeButtonIconStyle\":\"light\",\"colorScheme\":{\"accent\":\"#007566\",\"background\":\"#001B0D\",\"label\":\"#FFFFFF\",\"primary\":\"#10C6B6\",\"secondaryLabel\":\"#FFFFFF\",\"seperator\":\"#FFFFFF\"},\"isFullScreen\":true,\"shouldShowAlertOnClose\":false,\"showCloseButtonAfter\":1,\"triggerPurchaseWithAlert\":false,\"triggerPurchaseWithProductChange\":false}}" } ``` | Name | Type | Required | Description | | ---- | ------ | -------- | ------------------------------------------------------------ | | lang | String | Yes |Locale code for the paywall localization. It uses language and region subtags separated by a hyphen (**-**).
Examples: `en` for English, `pt-br` for Brazilian Portuguese.
Refer to [Localizations and locale codes](localizations-and-locale-codes) for more details.
| | data | String | Yes | Serialized JSON string representing the remote config of your paywall. You can find it in the **Remote Config** tab of a specific paywall in the Adapty Dashboard. | ## Subscription Info about your end user subscription. You can do the following action via Adapty server-side API: - [Check the user's current subscription](ss-get-profile) by retrieving their profile details - [Set transaction to your user](ss-set-transaction) and grant a subscription to them | Parameter | Type | Required | Nullable | Description | | :---------------------------- | :------------ | -------- | -------- | :----------------------------------------------------------- | | purchase_type | String | Yes | No | The type of product purchased. Possible value: `subscription`. | | store | String | Yes | No | Store where the product was bought. Options include `app_store`, `play_store`, `stripe`, or the Store ID of your [custom store](custom-store). | | environment | String | No | No | Environment where the transaction took place. Options are `Sandbox` or `Production`. `Production` is used by default. | | store_product_id | String | Yes | No | ID of the product in the app store (App Store, Google Play, Stripe, etc.) that unlocked this access level. | | store_transaction_id | String | Yes | No | Transaction ID in the app store (App Store, Google Play, Stripe, etc.). | | store_original_transaction_id | String | Yes | No |For subscriptions, this ID links to the first transaction in a renewal chain. Each renewal is connected to this original transaction.
If there's no renewal, `store_original_transaction_id` matches `store_transaction_id`.
| | offer | Object | No | Yes | The offer used in the purchase, provided as an [Offer](server-side-api-objects#offer) object. | | is_family_shared | Boolean | No | No | 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. `false` is used by default. | | price | Object | Yes | No | Price of the subscription or purchase as a [Price](server-side-api-objects#price) object. An initial subscription purchase with zero cost is a free trial; a renewal with zero cost is a free renewal. | | purchased_at | ISO 8601 date | Yes | No | The datetime of the most recent access level purchase. | | refunded_at | ISO 8601 date | No | No | The datetime when the subscription was refunded, if applicable. | | cancellation_reason | String | No | No | Possible reasons for cancellation include: `voluntarily_cancelled`, `billing_error`, `price_increase`, `product_was_not_available`, `refund`, `upgraded`, or `unknown`. | | variation_id | String | No | No | The variation ID used to trace purchases to the specific paywall they were made from. | | originally_purchased_at | ISO 8601 date | Yes | No | For subscription chains, this is the purchase date of the original transaction, linked by `store_original_transaction_id`. | | expires_at | ISO 8601 date | Yes | No | The datetime when the access level expires. It may be in the past and `null` for lifetime access. | | renew_status | Boolean | Yes | No | Indicates if auto-renewal is enabled for the subscription. | | renew_status_changed_at | ISO 8601 date | No | No | The datetime when auto-renewal was either enabled or disabled. | | billing_issue_detected_at | ISO 8601 date | No | No | The datetime when a billing issue was detected (e.g., a failed card charge). The subscription might still be active. This is cleared if the payment goes through. | | grace_period_expires_at | ISO 8601 date | No | No | The datetime when the [grace period](https://developer.apple.com/news/?id=09122019c) will end if the subscription is currently in one. | :::note Although the SDK includes the `is_active` parameter to check if a subscription is active, the server-side API does not provide this parameter. However, you can determine subscription status at any time by checking whether the current date falls between the `starts_at` and `expires_at` parameters of the [Access Level](#access-level) object. ::: --- # File: server-side-api-specs-legacy.md --- --- title: "Legacy server-side API specs" description: "" --- **Base URL**: `https://api.adapty.io/api/v1/sdk` ## Authorization :::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). ::: Each API request must be signed with the [Secret Key](general). When calling API: - You must set **Authorization** header with the value "Api-Key \{secret\_token\}" \(without quotes\) to each request, for example `Api-Key secret_live_BEHrYLTr.ce5zuDEWz06lFRNiaJC8mrLtL8fUwswD` - Use JSON payload in the request body for POST and PATCH requests - All requests must set header **Content-Type**: application/json ## Working with customer user ID :::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). ::: Most server-side API requests allow passing `customer_user_id` as a URL parameter. This makes it easy for you to query/update data in Adapty, without having to store Adapty's `profile_id`. In most cases, you should pass `customer_user_id` as is, without any modifications. However, if your `customer_user_id` contains [reserved URI characters](https://en.wikipedia.org/wiki/Percent-encoding), for example `/`, `?`, `+` you should pass it encoded. Use the [Base64URL](https://www.base64encode.org/) encoding (not the regular Base64). This way, all special characters will be encoded and Adapty will decode it upon receiving. To tell Adapty the `customer_user_id` is encoded, pass the `is_user_id_base64url_encoded=1` get parameter. Note, that passing the `is_user_id_base64url_encoded=1` get parameter without actual encoding, will end up with 400 validation error. You should only encode the `customer_user_id` if you pass it as a URL path. When sending `customer_user_id` inside the JSON payload (for example, when creating the profile), you should not encode it. ```python showLineNumbers title="Python" ## Don't encode customer_user_id = '123' # GET: /profiles/123/ customer_user_id = 'abc' # GET: /profiles/abc/ customer_user_id = '3c410419-9959-447a-84b5-be7cb6a308d9' # GET: /profiles/3c410419-9959-447a-84b5-be7cb6a308d9/ ## Base64URL encode customer_user_id = '123+456' # GET: /profiles/MTIzKzQ1Ng==/?is_user_id_base64url_encoded=1 customer_user_id = 'abc/def' # GET: /profiles/YWJjL2RlZg==/?is_user_id_base64url_encoded=1 customer_user_id = '012?012' # GET: /profiles/MDEyPzAxMg==/?is_user_id_base64url_encoded=1 ``` ## Requests ### Prolong/grant a subscription for a user :::warning **You are viewing the guide for the legacy server-side API.** For the latest version, refer to the following requests: - [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. ::: ```http POST: /profiles/{profile_id_or_customer_user_id}/paid-access-levels/{access_level}/grant/ ``` Path parameters: | Param | Type | Required | Nullable | Description | | :--------------------------------- | :--- | :------- | :------- | :-------------------------------------------------------------- | | **profile_id_or_customer_user_id** | str | ✅ | ❌ | Adapty profile ID or developer's internal ID | | **access_level** | str | ✅ | ❌ | ID \(slug\) of a paid access level. Find it in Adapty Dashboard | Request parameters: | Param | Type | Required | Nullable | Description | | ---------------------------------- | ------------- | ------------- | -------- | ------------------------------------------------------------ | | **expires_at** | ISO 8601 date | ✅\* see below | ❌ | Subscription deadline | | **duration_days** | int | ✅\* see below | ❌ | Additional days to a current subscription\*\* | | **is_lifetime** | bool | ✅\* see below | ❌ | If set true, then a user will forever have a paid access level forever | | **starts_at** | ISO 8601 date | ❌ | ❌ | If the start time of the action is in the future, then you can transfer it. If the start time and the period are indicated, the period will be counted from the indicated time | | **vendor_product_id** | str | ❌ | ❌ | When posting a transaction, include the product ID that triggers the subscription renewal. If you're granting an access level without a transaction, skip this parameter, and **adapty_server_side_product** will be used by default. | | **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. | | **vendor_original_transaction_id** | str | ❌ | ❌ | ID of the original transaction in the subscription renewal chain in a vendor environment. | | **vendor_transaction_id** | str | ❌ | ❌ |Transaction ID in a vendor environment.
If it is the same as **vendor_original_transaction_id** or if **vendor_original_transaction_id** is absent, Adapty considers it the first subscription purchase. If it differs from **vendor_original_transaction_id**, Adapty considers the purchase the subscription renewal.
| | **store** | str | ❌ | ❌ | A store where users purchased a product, such as **app\_store** and **play\_store**, can be custom. Default is **adapty** | | **introductory_offer_type** | str | ❌ | ❌ | A type of introduction offer. Available values are **free\_trial**, **pay\_as\_you\_go**, and **pay\_up\_front**. | | **price** | float | ❌ | ❌ |Price of the subscription/purchase to save in [transaction](server-side-api-specs-legacy#transaction).
The first subscription purchase with a zero price is considered a free trial, while a renewal with a zero price is considered a free subscription renewal.
If you provide price, provide `price_locale` as well.
| | **price_locale** | str | ❌ | ❌ | The currency of the transaction in the [three-letter](https://en.wikipedia.org/wiki/ISO_4217) format. `USD` is used by default. | | **proceeds** | float | ❌ | ❌ | Proceeds \(price that is reduced due to stores' fee\) of the subscription/purchase to save in [transaction](server-side-api-specs-legacy#transaction). | | **is_sandbox** | bool | ❌ | ❌ | Boolean indicating whether the product was purchased in the sandbox or production environment. | #### Paid access level There are three ways to grant users a subscription. So, at least one of **is\_lifetime**, **expires\_at**, or **duration\_days** must be set. If more than one param is set, then **is\_lifetime=true** has a maximum priority, then **expires\_at**, and lastly **duration\_days**. As all payment processing is done by Apple/Google, Adapty can not control or affect it. So, when using **duration\_days** to a current subscription, remember that a user still will be charged on a needed day. For example, the user has a monthly subscription and the next charge date will be the 5th of April. You grant a user additional 7 days, _but the user still is charged on the 5th of April!._ It's best using **duration\_days** with never subscribed users or churned. In that case, reference day is a day of granting. #### Transaction If all **vendor_product_id**, **vendor\_transaction\_id,** and **store** are specified, Adapty creates and saves transaction entry so this grant is accounted in [Charts](analytics-charts) \(Revenue, MRR, Subscriptions\) unless the transaction with these parameters already exists \(e.g. it was generated by iOS purchase\). If **price** is specified, it is associated with this transaction. Currently, this type of transaction does not generate any profile events, does not affect users' subscription status, and does not show up in the [**Event Feed**](https://app.adapty.io/event-feed). Also, be aware that these transactions affect billing since they are counted towards MTR. Sample request: ```json showLineNumbers title="Json" { "starts_at": "2020-01-15T15:10:36.517975+0000", "expires_at": "2020-02-15T15:10:36.517975+0000", "vendor_product_id": "basic_subscription_1_month", "vendor_transaction_id": "1000000630116569", "store": "app_store", "introductory_offer_type": null } ``` Sample response: ```json showLineNumbers title="Json" { "data": { "app_id": "ff90dd2e-e7f2-454b-9d86-071036a284fe", "profile_id": "77112400-89f1-4465-b9c9-5437e58c6688", "customer_user_id": "iwitaly@adapty.io", "paid_access_levels": { "premium": { "id": "premium", "is_active": true, "is_lifetime": false, "expires_at": "2023-03-29T15:30:34.000000+0000", "starts_at": null, "will_renew": false, "vendor_product_id": "adapty_server_side_product", "base_plan_id": "premium_autorenewing", "vendor_transaction_id": "1000000630116569", "vendor_original_transaction_id": "1000000625263604", "store": "adapty", "activated_at": "2020-03-26T16:24:19.497674+0000", "renewed_at": "2020-03-26T16:24:19.497674+0000", "unsubscribed_at": null, "billing_issue_detected_at": null, "is_in_grace_period": false, "active_introductory_offer_type": "free_trial", "active_promotional_offer_type": null, "active_promotional_offer_id": null, "cancellation_reason": null } }, "subscriptions": { "com.adapty.premium.monthly": { "is_active": false, "is_lifetime": false, "expires_at": "2020-02-21T16:30:34.000000+0000", "starts_at": null, "will_renew": false, "vendor_product_id": "com.adapty.premium.monthly", "base_plan_id": "monthly_autorenewing", "vendor_transaction_id": "1000000630116569", "vendor_original_transaction_id": "1000000625263604", "store": "app_store", "activated_at": "2020-02-10T19:14:02.000000+0000", "renewed_at": "2020-02-21T16:25:34.000000+0000", "unsubscribed_at": "2020-02-21T16:30:34.000000+0000", "billing_issue_detected_at": "2020-02-21T16:30:34.000000+0000", "is_in_grace_period": false, "active_introductory_offer_type": null, "active_promotional_offer_type": null, "active_promotional_offer_id": null, "cancellation_reason": "voluntarily_cancelled", "is_sandbox": true }, "com.adapty.premium.weekly": { "is_active": false, "is_lifetime": false, "expires_at": "2020-02-10T19:32:00.000000+0000", "starts_at": null, "will_renew": true, "vendor_product_id": "com.adapty.premium.weekly", "base_plan_id": "weekly_autorenewing", "vendor_transaction_id": "1000000625265713", "vendor_original_transaction_id": "1000000625263604", "store": "app_store", "activated_at": "2020-02-10T19:14:02.000000+0000", "renewed_at": "2020-02-10T19:29:00.000000+0000", "unsubscribed_at": null, "billing_issue_detected_at": null, "is_in_grace_period": false, "active_introductory_offer_type": null, "active_promotional_offer_type": null, "active_promotional_offer_id": null, "cancellation_reason": null, "is_sandbox": true }, "basic_subscription_unlimited": { "is_active": true, "is_lifetime": false, "expires_at": "2021-02-27T11:00:30.000000+0000", "starts_at": null, "will_renew": false, "vendor_product_id": "basic_subscription_unlimited", "base_plan_id": "basic_prepaid", "vendor_transaction_id": "1000000632277988", "vendor_original_transaction_id": "1000000632277988", "store": "app_store", "activated_at": "2020-02-27T11:00:30.000000+0000", "renewed_at": null, "unsubscribed_at": null, "billing_issue_detected_at": null, "is_in_grace_period": false, "active_introductory_offer_type": null, "active_promotional_offer_type": null, "active_promotional_offer_id": null, "cancellation_reason": null, "is_sandbox": true } }, "non_subscriptions": null } } ``` Learn more about responses in the [API Objects](server-side-api-objects) section**.** ### Revoke subscription from a user :::warning **You are viewing the guide for the legacy server-side API.** For the latest version, refer to the [Revoke Access Level](ss-revoke-access-level) request which can both shorten or revoke access without a transaction. ::: ```http POST: /profiles/{profile_id_or_customer_user_id}/paid-access-levels/{access_level}/revoke/ ``` Path parameters: | Param | Type | Required | Nullable | Description | | :--------------------------------- | :--- | :------- | :------- | :----------------------------------------------------------- | | **profile_id_or_customer_user_id** | str | ✅ | ❌ | Adapty profile ID or developer's internal ID | | **access_level** | str | ✅ | ❌ | ID (slug) of a paid access level. Find it in Adapty Dashboard | Request parameters: | Param | Type | Required | Nullable | Description | | :------------- | :--- | :------- | :------- | :--------------------------------------------------- | | **is\_refund** | bool | ✅ | ❌ | Whether this subscription is revoked due to a refund | Revokes user's subscription by setting **unsubscribed\_at** to current datetime, and **expires\_at** to a maximum of current **starts\_at** and current datetime \(to avoid **expires\_at** being less than **starts\_at**). If there is a [transaction](server-side-api-specs-legacy#prolonggrant-a-subscription-for-a-user) associated with this paid access level, this transaction expiration is also set to the new **expires\_at** value. If **is\_refund** is **true**, the transaction is marked as a refund, and revenue is set to zero. ### Validate a purchase from Stripe, provide access level to a customer, and import his transaction history from Stripe ```http POST: /api/v1/sdk/purchase/stripe/token/validate/ ``` :::warning This request must use a different Content-Type: `Content-Type: application/vnd.api+json'` ::: Request parameters: | Param | Type | Required | Nullable | Description | | :--------------------- | :--- | :------- | :------- | :----------------------------------------------------------- | | **customer\_user\_id** | str | ✅ | ❌ | Developer's internal customer ID | | **stripe\_token** | str | ✅ | ❌ | Token of a Stripe object that represents a unique purchase. Could either be a token of Stripe's Subscription (`sub_XXX`) or Payment Intent (`pi_XXX`). | Sample request: ```json showLineNumbers title="CURL" curl --location 'https://api.adapty.io/api/v1/sdk/purchase/stripe/token/validate/' \ --header 'Content-Type: application/vnd.api+json' \ --header 'Authorization: Api-Key1. **You set up your endpoint:** 1. Ensure your server can process Adapty requests with the **Content-Type** header set to `application/json`. 2. Configure your server to receive Adapty's verification request and respond with any `2xx` status and a JSON body. 3. [Handle subscription events](#subscription-events) once the connection is verified. 2. **You configure and enable the webhook integration** in the [Adapty Dashboard](#configure-webhook-integration-in-the-adapty-dashboard). You can also [map Adapty events to custom event names](#configure-webhook-integration-in-the-adapty-dashboard). We recommend testing in the **Sandbox environment** before switching to production. 3. **Adapty sends a verification request** to your server. 4. **Your server responds** with a `2XX` status and a JSON body. 5. **Once Adapty receives a valid response, it starts sending subscription events.** ## Set up your server to process Adapty requests Adapty will send to your webhook endpoint 2 types of requests: 1. [Verification request](#verification-request): the initial request to verify the connection is set up correctly. This request will not contain any event and will be sent the moment you click the **Save** button in the Webhook integration of the Adapty Dashboard. To confirm your endpoint successfully received the verification request, your endpoint should answer with the verification response. 2. [Subscription event](#subscription-events): A standard request Adapty server sends every time an event is created in it. Your server does not need to reply with any specific response. The only thing the Adapty server needs is to receive a standard 200-code HTTP response if it successfully receives the message. ### Verification request After you enable webhook integration in the Adapty Dashboard, Adapty will send a POST verification request containing an empty JSON object `{}` as a body. Set up your endpoint to have the **Content-Type header** as `application/json`, i.e. your server's endpoint should expect the incoming webhook request to have its payload formatted as JSON. Your server must reply with a 2xx status code and send any valid JSON response, for example: ```json title="Json" {} ``` Once Adapty receives the verification response in the correct format and with a 2xx status code, your Adapty webhook integration is fully configured. ### Subscription events Subscription events are sent with the **Content-Type** header set to `application/json` and contain event data in JSON format. For possible event types and request structures, see [Webhook event types and fields](webhook-event-types-and-fields). ## Configure webhook integration in the Adapty Dashboard Within Adapty, you can configure separate flows for production events and test events received from the Apple or Stripe sandbox environment or Google test account. For production events, use the **Production endpoint URL** field specifying the URL to which the callbacks will be sent. Additionally, configure the **Authorization header value for production endpoint** field - the header for your server to authenticate Adapty events. Note that we'll use the value specified in the **Authorization header value for production endpoint** field as the `Authorization` header exactly as provided, without any changes or additions. For test events, employ the **Sandbox endpoint URL** and **Authorization header value for sandbox endpoint** fields accordingly. To set up the webhook integration: 1. Open [Integrations -> Webhook](https://app.adapty.io/integrations/customwebhook) in your Adapty Dashboard. 2. Turn on the toggle to initiate the integration. 4. Fill out the integration fields: | Field | Description | | ------------------------------------------------------ | ------------------------------------------------------------ | | **Production endpoint URL** | The URL Adapty uses to send HTTP POST requests for events in production. | | **Authorization header value for production endpoint** |
The header that your server will use to authenticate requests from Adapty in production. Note that we'll use the value specified in this field as the `Authorization` header exactly as provided, without any changes or additions.
Although not mandatory, it's strongly recommended for enhanced security.
| Additionally, for your testing needs in the sandbox environment, two other fields are available: | Testing field | Description | | --------------------------------------------------- | ------------------------------------------------------------ | | **Sandbox endpoint URL** | The URL Adapty uses to send HTTP POST requests for events in the sandbox environment. | | **Authorization header value for sandbox endpoint** |The header that your server will use to authenticate requests from Adapty during testing in the sandbox environment. Note that we'll use the value specified in this field as the `Authorization` header exactly as provided, without any changes or additions.
Although not mandatory, it's strongly recommended for enhanced security.
| 4. (optional) Pick the events you want to receive and map their names. Check out our [Event flows](event-flows) to see which events are triggered in different situations. If your event IDs differ from those used in Adapty, keep the IDs in your system as is and replace the default Adapty event IDs with yours in the **Events names** section of the [Integrations -> Webhooks](https://app.adapty.io/integrations/customwebhook) page. The event ID can be any string; simply make sure the event ID in your webhook processing server coincides with the one you entered in the Adapty Dashboard. You cannot leave the event ID empty for enabled events. 5. Additional fields and options are not obligatory; use them as needed: | Setting | Description | | :--------------------------------- | :----------------------------------------------------------- | | **Send Trial Price** | When enabled, Adapty will include the subscription price in the `price_local` and `price_usd` fields 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. | | **Send user attributes** | Enable this option to send user-specific attributes, such as language preferences. These attributes will appear in the `user_attributes` field. See [Event fields](webhook-event-types-and-fields#event-fields) for more information. | | **Send attribution** | Turn on this option to include attribution information (e.g., AppsFlyer data) in the `attributions` field. Refer to the [Attribution data](webhook-event-types-and-fields#attribution-data) section for details. | | **Send Play Store purchase token** | Turn on this option to receive the Play Store token required for purchase revalidation, if needed. Enabling it will add the `play_store_purchase_token` parameter to the event. For details on its content, refer to the [Play Store purchase token](webhook-event-types-and-fields#play-store-purchase-token) section. | 6. Remember to click the **Save** button to confirm the changes. The moment you click the **Save** button, Adapty will send a verification request and will wait for your server verification response. ### Choose events to send and map event names Choose the events you want to receive in your server by enabling the toggle next to it. If your event names differ from those used in Adapty and you need to keep your names as is, you can set up the mapping by replacing the default Adapty event names with your own in the **Events names** section of the [Integrations -> Webhooks](https://app.adapty.io/integrations/customwebhook) page. The event name can be any string. You cannot leave the fields empty for enabled events. If you accidentally removed Adapty event name, you can always copy the name from the [Events to send to third-party integrations](events) topic. ## Handle webhook events Webhooks are typically delivered within 5 to 60 seconds after the event occurs. Cancellation events, however, may take up to 2 hours to be delivered after a user cancels their subscription. If your server's response status code is outside the 200-404 range, Adapty will retry sending the event up to 9 times over 24 hours with exponential backoff. We suggest you set up your webhook to do only basic validation of the event body from Adapty before responding. If your server can't process the event and you don't want Adapty to retry, use a status code within the 200-404 range. Also, handle any time-consuming tasks asynchronously and respond to Adapty quickly. If Adapty doesn't receive a response within 10 seconds, it will consider the attempt a failure and will retry. --- # File: setting-user-attributes.md --- --- title: "Set user attributes in iOS SDK" description: "Learn how to set user attributes in Adapty to enable better audience segmentation." --- You can set optional attributes such as email, phone number, etc, to the user of your app. You can then use attributes to create user [segments](segments) or just view them in CRM. ### Setting user attributes To set user attributes, call `.updateProfile()` method: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. ```swift showLineNumbers do { builder = try builder.with(customAttribute: "value1", forKey: "key1") } catch { // handle key/value validation error } ``` To remove existing key, use `.withRemoved(customAttributeForKey:)` method: ```swift showLineNumbers do { builder = try builder.withRemoved(customAttributeForKey: "key2") } catch { // handle error } ``` Sometimes you need to figure out what custom attributes have already been installed before. To do this, use the `customAttributes` field of the `AdaptyProfile` object. :::warning Keep in mind that the value of `customAttributes` may be out of date since the user attributes can be sent from different devices at any time so the attributes on the server might have been changed after the last sync. ::: ### Limits - Up to 30 custom attributes per user - Key names are up to 30 characters long. The key name can include alphanumeric characters and any of the following: `_` `-` `.` - Value can be a string or float with no more than 50 characters. --- # File: singular.md --- --- title: "Singular" description: "Integrate Singular with Adapty to analyze marketing and subscription data." --- [Singular](https://www.singular.net/) 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 Singular and analyze precisely how much revenue your campaigns generate. Adapty can send all subscription events which are configured in your integration to Singular. As a result, you'll be able to track these events within the Singular dashboard. This integration is beneficial for evaluating the effectiveness of your advertising campaigns. ## How to set up Singular integration To set up the integration with Singular, go to [Integrations > Singular](https://app.adapty.io/integrations/singular) in your Adapty Dashboard, turn on a toggle, and fill out the fields. For this integration to work, the Singular SDK Key is required. It can be found in Singular dashboard: Developer tools -> SDK Keys -> SDK Key (**not** SDK Secret): Below the credentials, there are three groups of events you can send to Singular from Adapty. 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 Singular using a server-to-server integration, allowing you to view all subscription events in your Singular dashboard and link them to your acquisition campaigns. :::warning Profiles created prior to configuring the integrations will not be able to deliver their events to Singular. ::: ## No need for SDK configuration There is no need to configure the SDK from your side at the moment — as Adapty already collects the data required by Singular and this integration is server-to-server. In case it ever changes, we'll let you know. --- # File: slack.md --- --- title: "Slack" description: "Integrate Slack with Adapty to receive real-time notifications about subscription events." --- [Slack](https://slack.com/) is a workplace messenger and productivity platform that probably needs no introduction. With this integration, you'll be able to be notified in Slack each time a revenue event is tracked by Adapty. This can be helpful if you like to cherish every moment your MRR increases or if you'd like to be on the lookout for trial cancellations, billing issues, refunds, and more. ## How to set up Slack integration You'll need to: - create an app in your Slack workspace - give it permission to post messages - and then provide the necessary info to Adapty in [Integrations → Slack](https://app.adapty.io/integrations/slack). ### 1\. Create an app in Slack 1. Go to [Slack API dashboard](https://api.slack.com/apps) and create an app like so: 2. Give it any name (`Adapty` for example) and add it to your workspace: ### 2\. Give permission to post and get a token for your app You'll be redirected to your app's page in Slack. 1. Scroll down and click **Permissions**: 2. After the redirect, scroll down to **Scopes** and click **Add an OAuth Scope**: 3. Give `chat:write`, `chat:write.public` and `chat:write.customize` permissions. Those are needed to post in your channels and customize the messages: 4. Scroll back to the top of the page and click **Install to Workspace**: 5. Click **Allow** here: After this, you'll be redirected to the same page, but you'll have an OAuth Token available (`xoxb-...`). This is exactly what's needed to complete the setup: ### 3\. Configure the integration in Adapty 1. Go to [**Integrations** → **Slack**](https://app.adapty.io/integrations/slack): 2. Paste the `xoxb-...` token from the previous step and choose which channels the app will post to. You can set up the integration to receive events only on production, sandbox or both. You can also choose which currency to post in (original or converted to USD). :::note Note that if you'd like to post messages from Adapty in a private channel, you'll need to manually add the `Adapty` app you've created in Slack to that channel. Otherwise, it would not work. ::: 3. Finally, you can choose which events you'd like to receive under **Events**: You're all set! The events will be sent to the channels you've specified. You'll be able to see the revenue where applicable and view the customer profile in Adapty: --- # File: splitmetrics.md --- --- title: "SplitMetrics Acquire" description: "Use SplitMetrics with Adapty for subscription A/B testing and optimization." --- With [SplitMetrics Acquire ](https://splitmetrics.com/acquire/)integration, you can see exactly how much money your Apple Search Ads make from subscriptions. And you can track your users for months to know how much money your ads make over time. In addition, Adapty sends [subscription events](events) to SplitMetrics Acquire so that you can build custom dashboards and automation there, based on Apple Search Ads attribution. It doesn't add any attribution data to Adapty, as we already have everything we need from ASA directly. ## How to set up SplitMetrics Acquire integration To integrate SplitMetrics Acquire go to [Integrations > SplitMetrics Acquire](https://app.adapty.io/integrations/splitmetrics) and set credentials. Open your SplitMetrics Acquire account, hover over one of the MMP logos, and click the **Settings** button. Find your Client ID in the dialog under item **5**, copy it, and then paste it to Adapty as **Client ID**. You will also have to set Apple App ID to use the integration. To find your App ID, open your app page in App Store Connect, go to the **App Information page** in the **General** section, and find the **Apple ID** in the left bottom part of the screen. ## Events and tags Below the credentials, there are three groups of events you can send to SplitMetrics Acquire 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 SplitMetrics Acquire using a server-to-server integration, allowing you to view all subscription events in your SplitMetrics dashboard. ## SDK configuration You don't have to configure anything on the SDK side, but we recommend sending `customerUserId` to Adapty for better accuracy. :::warning 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, SplitMetrics Acquire won't work. ::: ## Troubleshooting If the integration with SplitMetrics Acquire isn't working despite the correct setup: - Make sure you've enabled 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), configured [Apple Search Ads](apple-search-ads) in Adapty, and [uploaded credentials](https://app.adapty.io/settings/apple-search-ads), without them, SplitMetric won't work. - Ensure the profiles have non-organic ASA attribution. Only profiles with detailed, non-organic ASA attribution will deliver their events to Asapty. --- # File: ss-200.md --- --- title: "Response to server-side API requests: 200: OK" description: "" displayed_sidebar: APISidebar --- The request is successful, the response will have the following data: ### Response structureThe request is successful. The response body contains the `data` field, which encapsulates the user's profile and associated information. | Parameter | Type | Nullable | Description | | --------- | ------ | ------------------ | ------------------------------------------------------------ | | data | Object | :heavy_minus_sign: | Contains the [Profile](server-side-api-objects#profile) object with user details and metadata. | ### data object structure The `data` field is the primary container for the user profile. It includes several fields:
The request failed because the profile in the request header wasn’t found. Double-check that there are no typos in the `profile_id` or `customer_user_id` you entered in the request header, and make sure it’s for the correct app. #### Body | Parameter | Type | Description | | ----------- | ------- | ------------------------------------------------------------ | | errors | Object |
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 |
The request failed because the specified profile wasn’t found. Double-check the `customer_user_id` or `profile_id` for any typos. ##### Body | Parameter | Type | Description | | ----------- | ------- | ------------------------------------------------------------ | | errors | Object |
The user’s Adapty profile ID. Visible in the **Adapty ID** field in the [Adapty Dashboard -> **Profiles**](https://app.adapty.io/profiles/users) -> specific profile page.
Interchangeable with **adapty-customer-user-id**, use any of them.
| | **adapty-customer-user-id** |The user's ID in your system. Visible in the **Customer user ID** field in the [Adapty Dashboard -> **Profiles**](https://app.adapty.io/profiles/users) -> specific profile page.
Interchangeable with **adapty-profile-id**, use any of them.
⚠️ Works only if you
Option to opt out of external analytics. When analytics is disabled, events won't be sent to integrations, and the fields `idfa`, `idfv`, and `advertising_id` will become nullable.
ON: External analytics is opted out for this user.
OFF: Analytics is active by default.
| | custom_attributes | Array | No | No |Allows setting up to 30 custom attributes for the profile. If you use the `custom_attributes` array, at least one pair of a key and value is required.
**Key:** Must be a string with no more than 30 characters, using only letters, numbers, dashes, periods, and underscores.
**Value:** Must be a string or float with no more than 30 characters. Booleans and integers will be converted to floats. To delete an attribute, send an empty value or `null`.
| | installation_meta | Object | No | No | Contains information about the specific app on a specific device, structured as an [Installation Meta](server-side-api-objects#installation-meta) object. | :::tip To authorize the request, make sure to include `profile_id` and/or `customer_user_id` in the header, as explained in the [Authorization](ss-authorization) section. - If you're adding a `customer_user_id` to a **new user profile**, include only the `customer_user_id` in the request header. This will create a new profile with a random `profile_id` and the specified `customer_user_id`. - If you're adding a `customer_user_id` to an **existing profile**, include both the `profile_id` and `customer_user_id` in the header. This will attach the `customer_user_id` to the existing profile. ::: --- ## Successful response: 200: OKThe request is successful. The response body contains the `data` field, which encapsulates the user's profile and associated information. | Parameter | Type | Nullable | Description | | --------- | ------ | ------------------ | ------------------------------------------------------------ | | data | Object | :heavy_minus_sign: | Contains the [Profile](server-side-api-objects#profile) object with user details and metadata. | ### data object structure The `data` field is the primary container for the user profile. It includes several fields:
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 |
The request failed because the specified profile wasn’t found. Double-check the `customer_user_id` or `profile_id` for any typos. ##### Body | Parameter | Type | Description | | ----------- | ------- | ------------------------------------------------------------ | | errors | Object |
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 |
The request failed because the specified profile wasn’t found. Double-check the `customer_user_id` or `profile_id` for any typos. ##### Body | Parameter | Type | Description | | ----------- | ------- | ------------------------------------------------------------ | | errors | Object |
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 |
The request failed because the specified profile wasn’t found. Double-check the `customer_user_id` or `profile_id` for any typos. ##### Body | Parameter | Type | Description | | ----------- | ------- | ------------------------------------------------------------ | | errors | Object |
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 |
The request is successful. The response body contains the `data` field, which encapsulates the user's profile and associated information. | Parameter | Type | Nullable | Description | | --------- | ------ | ------------------ | ------------------------------------------------------------ | | data | Object | :heavy_minus_sign: | Contains the [Profile](server-side-api-objects#profile) object with user details and metadata. | ### data object structure The `data` field is the primary container for the user profile. It includes several fields:
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 |
The request failed because the specified profile wasn’t found. Double-check the `customer_user_id` or `profile_id` for any typos. ##### Body | Parameter | Type | Description | | ----------- | ------- | ------------------------------------------------------------ | | errors | Object |
The request failed because the profile in the request header wasn’t found. Double-check that there are no typos in the `profile_id` or `customer_user_id` you entered in the request header, and make sure it’s for the correct app. #### Body | Parameter | Type | Description | | ----------- | ------- | ------------------------------------------------------------ | | errors | Object |
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 |
The request is successful. The response body contains the `data` field, which encapsulates the user's profile and associated information. | Parameter | Type | Nullable | Description | | --------- | ------ | ------------------ | ------------------------------------------------------------ | | data | Object | :heavy_minus_sign: | Contains the [Profile](server-side-api-objects#profile) object with user details and metadata. | ### data object structure The `data` field is the primary container for the user profile. It includes several fields:
The request failed because the profile in the request header wasn’t found. Double-check that there are no typos in the `profile_id` or `customer_user_id` you entered in the request header, and make sure it’s for the correct app. #### Body | Parameter | Type | Description | | ----------- | ------- | ------------------------------------------------------------ | | errors | Object |
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 |
The request failed because the specified profile wasn’t found. Double-check the `customer_user_id` or `profile_id` for any typos. ##### Body | Parameter | Type | Description | | ----------- | ------- | ------------------------------------------------------------ | | errors | Object |
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 |
The request is successful. The response body contains the `data` field, which encapsulates the user's profile and associated information. | Parameter | Type | Nullable | Description | | --------- | ------ | ------------------ | ------------------------------------------------------------ | | data | Object | :heavy_minus_sign: | Contains the [Profile](server-side-api-objects#profile) object with user details and metadata. | ### data object structure The `data` field is the primary container for the user profile. It includes several fields:
The request failed because the profile in the request header wasn’t found. Double-check that there are no typos in the `profile_id` or `customer_user_id` you entered in the request header, and make sure it’s for the correct app. #### Body | Parameter | Type | Description | | ----------- | ------- | ------------------------------------------------------------ | | errors | Object |
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 |
The request failed because the specified profile wasn’t found. Double-check the `customer_user_id` or `profile_id` for any typos. ##### Body | Parameter | Type | Description | | ----------- | ------- | ------------------------------------------------------------ | | errors | Object |
The request failed because the profile in the request header wasn’t found. Double-check that there are no typos in the `profile_id` or `customer_user_id` you entered in the request header, and make sure it’s for the correct app. #### Body | Parameter | Type | Description | | ----------- | ------- | ------------------------------------------------------------ | | errors | Object |
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 |
For subscriptions, this ID links to the first transaction in a renewal chain. Each renewal is connected to this original transaction.
If there's no renewal, `store_original_transaction_id` matches `store_transaction_id`.
| | offer | Object | No | Yes | The offer used in the purchase, provided as an [Offer](server-side-api-objects#offer) object. | | is_family_shared | Boolean | No | No | 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. `false` is used by default. | | price | Object | Yes | No | Price of the subscription or purchase as a [Price](server-side-api-objects#price) object. An initial subscription purchase with zero cost is a free trial; a renewal with zero cost is a free renewal. | | purchased_at | ISO 8601 date | Yes | No | The datetime of the most recent access level purchase. | | refunded_at | ISO 8601 date | No | No | The datetime when the subscription was refunded, if applicable. | | cancellation_reason | String | No | No | Possible reasons for cancellation include: `voluntarily_cancelled`, `billing_error`, `price_increase`, `product_was_not_available`, `refund`, `upgraded`, or `unknown`. | | variation_id | String | No | No | The variation ID used to trace purchases to the specific paywall they were made from. | | originally_purchased_at | ISO 8601 date | Yes | No | For subscription chains, this is the purchase date of the original transaction, linked by `store_original_transaction_id`. | | expires_at | ISO 8601 date | Yes | No | The datetime when the access level expires. It may be in the past and `null` for lifetime access. | | renew_status | Boolean | Yes | No | Indicates if auto-renewal is enabled for the subscription. | | renew_status_changed_at | ISO 8601 date | No | No | The datetime when auto-renewal was either enabled or disabled. | | billing_issue_detected_at | ISO 8601 date | No | No | The datetime when a billing issue was detected (e.g., a failed card charge). The subscription might still be active. This is cleared if the payment goes through. | | grace_period_expires_at | ISO 8601 date | No | No | The datetime when the [grace period](https://developer.apple.com/news/?id=09122019c) will end if the subscription is currently in one. | ### For one-time purchase | Parameter | Type | Required | Nullable | Description | | :---------------------------- | :------------ | -------- | -------- | :----------------------------------------------------------- | | purchase_type | String | Yes | No | The type of product purchased. Possible value: `one_time_purchase`. | | store | String | Yes | No | Store where the product was bought. Possible values: `app_store`, `play_store`, `stripe`, or the Store ID of your [custom store](custom-store). | | environment | String | No | No | Transaction environment that provided the access level. Options: `Sandbox`, `Production`. `Production` is used by default. | | store_product_id | String | Yes | No | The product ID in the app store (App Store, Google Play, Stripe, etc.) that unlocked this access level. | | store_transaction_id | String | Yes | No | Transaction ID in the app store (App Store, Google Play, Stripe, etc.). | | store_original_transaction_id | String | Yes | No |For recurring subscriptions, this is the original transaction ID that links the chain of renewals. The original transaction is the first in the chain; later transactions are renewals.
If there's no renewal, `store_original_transaction_id` matches `store_transaction_id`.
| | offer | Object | No | Yes | The offer used for the purchase as an [Offer](server-side-api-objects#offer) object. | | is_family_shared | Boolean | No | No | 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. `false` is used by default. | | price | Object | Yes | No | Price of the one-time purchase as a [Price](server-side-api-objects#price) object. An initial subscription purchase with zero cost is a free trial; a renewal with zero cost is a free renewal. | | purchased_at | ISO 8601 date | Yes | No | The datetime when the access level was last purchased. | | refunded_at | ISO 8601 date | No | No | If refunded, shows the datetime of the refund. | | cancellation_reason | String | No | No | Possible reasons for cancellation: `voluntarily_cancelled`, `billing_error`, `price_increase`, `product_was_not_available`, `refund`, `cancelled_by_developer`, `new_subscription`, `unknown`. | | variation_id | String | No | No | The variation ID used to trace purchases to the specific paywall they were made from. | --- ## Successful response: 200: OKThe request is successful. The response body contains the `data` field, which encapsulates the user's profile and associated information. | Parameter | Type | Nullable | Description | | --------- | ------ | ------------------ | ------------------------------------------------------------ | | data | Object | :heavy_minus_sign: | Contains the [Profile](server-side-api-objects#profile) object with user details and metadata. | ### data object structure The `data` field is the primary container for the user profile. It includes several fields:
The request failed because the profile in the request header wasn’t found. Double-check that there are no typos in the `profile_id` or `customer_user_id` you entered in the request header, and make sure it’s for the correct app. #### Body | Parameter | Type | Description | | ----------- | ------- | ------------------------------------------------------------ | | errors | Object |
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 |
The request failed because the specified profile wasn’t found. Double-check the `customer_user_id` or `profile_id` for any typos. ##### Body | Parameter | Type | Description | | ----------- | ------- | ------------------------------------------------------------ | | errors | Object |
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 |
Option to opt out of external analytics. When analytics is disabled, events won't be sent to integrations, and the fields `idfa`, `idfv`, and `advertising_id` will become nullable.
ON: External analytics is opted out for this user.
OFF: Analytics is active by default.
| | custom_attributes | Array | No | No |Allows setting up to 30 custom attributes for the profile. If you use the `custom_attributes` array, at least one pair of a key and value is required.
**Key:** Must be a string with no more than 30 characters, using only letters, numbers, dashes, periods, and underscores.
**Value:** Must be a string or float with no more than 30 characters. Booleans and integers will be converted to floats. To delete an attribute, send an empty value or `null`.
| | installation_meta | Object | No | No | Contains information about the specific app on a specific device, structured as an [Installation Meta](server-side-api-objects#installation-meta) object. | :::tip `profile_id` and/or `customer_user_id` must be included in the request header, as described in the [Authorization](ss-authorization) section. If you're adding a `customer_user_id` to an existing profile: 1. Use the `POST` method. 2. Add both `profile_id` and `customer_user_id` to the request header. This will link the `customer_user_id` to the user's existing profile. ::: --- ## Successful response: 200: OKThe request is successful. The response body contains the `data` field, which encapsulates the user's profile and associated information. | Parameter | Type | Nullable | Description | | --------- | ------ | ------------------ | ------------------------------------------------------------ | | data | Object | :heavy_minus_sign: | Contains the [Profile](server-side-api-objects#profile) object with user details and metadata. | ### data object structure The `data` field is the primary container for the user profile. It includes several fields:
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 |
The request failed because the specified profile wasn’t found. Double-check the `customer_user_id` or `profile_id` for any typos. ##### Body | Parameter | Type | Description | | ----------- | ------- | ------------------------------------------------------------ | | errors | Object |
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 |
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:Two-way transmission:
A unique identifier set by you to identify your users in your system. This could be the user's email, your internal ID, or any other string. To use this option, you must
It is the best choice for identifying a test device, especially if you're using several devices for the same account. All the devices with this account will be considered test.
| | Adapty profile ID |A unique identifier for the [user profile](profiles-crm) in Adapty.
Use it if you cannot use Customer User ID, IDFA for iOS, or Advertising ID for Android. Note that the Adapty Profile ID can change if you reinstall the app or re-log in.
| #### How to obtain Customer User ID and Adapty profile ID Both identifiers can be obtained in the **Profile** details of the Adapty Dashboard: 1. Find the user's profile in the [**Adapty Profiles** -> **Event feed** tab](https://app.adapty.io/event-feed). :::note To find the exact profile, make a rare type of transaction. In this case, once the transaction appears in the [**Event Feed**](https://app.adapty.io/event-feed), you'll easily identify it. ::: 2. Copy **Customer user ID** and **Adapty ID** field values in the profile details: ### Apple identifiers | Identifier | Usage | |----------|-----| | IDFA |The Identifier for Advertisers (IDFA) is a unique device identifier assigned by Apple to a user’s device.
It's ideal for iOS devices as it never changes on its own, although you can manually reset it.
**Note**: Since the rollout of iOS 14.5, advertisers must ask for user consent to access the IDFA. Ensure you are asking for consent in your app and you have provided it on your test device.
| | IDFV | The Identifier for Vendors (IDFV) is a unique alphanumeric identifier assigned by Apple to all apps on a single device from the same publisher/vendor. It can change if you reinstall or update your app. | #### How to obtain the IDFA Apple does not provide the IDFA by default. Obtain it from the profile attribution in the Adapty Dashboard: 1. Find the user's profile in the [**Adapty Profiles** -> **Event feed** tab](https://app.adapty.io/event-feed). :::note To find the exact profile, make a rare type of transaction. In this case, once the transaction appears in the [**Event Feed**](https://app.adapty.io/event-feed), you'll easily identify it. ::: 2. Open the profile details and copy the **IDFA** field value in the **Attributes** section: Alternatively, you can [find the app on the App Store that will show your IDFA to you](https://www.apple.com/us/search/idfa?src=globalnav). #### How to obtain the Identifier for vendors (IDFV) To obtain the IDFV, ask your developer to request it using the following method for your app and display the received identifier to your logs or debug panel. ```swift showLineNumbers title="Swift" UIDevice.current.identifierForVendor ``` ### Google identifiers | Identifier | Usage | |----------|-----| | Advertising ID |The Advertising ID is a unique device identifier assigned by Google to a user’s device.
It's ideal for Android devices as it never changes on its own, although you can manually reset it.
**Note**: To use it, turn off the **Opt out of Ads Personalization** in your **Ads** settings if you use Android 12 or higher.
| | Android ID | The Android ID is a unique identifier for each combination of app-signing key, user, and device. Available on Android 8.0 and higher versions. | #### How to obtain Advertising ID To find your device's advertising ID: 1. Open the **Settings** app on your Android device. 2. Click on **Google**. 3. Select **Ads** under **Services**. Your advertising ID will be listed at the bottom of the screen. #### How to obtain Android ID To obtain the Android ID, ask your developer to request the [ANDROID_ID](https://developer.android.com/reference/android/provider/Settings.Secure#ANDROID_ID) using the following method for your app and display the received identifier in your logs or debug panel. ```kotlin showLineNumbers title="Kotlin/Java" android.provider.Settings.Secure.getString(contentResolver, android.provider.Settings.Secure.ANDROID_ID); ``` --- # File: test-purchases-in-sandbox.md --- --- title: "Test in-app purchases in App Store Sandbox" description: "Test purchases in the sandbox environment to ensure smooth transactions." --- Once you've configured in-app purchases in your mobile app, it's crucial to test them thoroughly to ensure functionality and proper transmission of transactions to Adapty before releasing the app to production. Transactions and purchases that occur in the sandbox don’t incur charges. To conduct sandbox testing, you'll need to use a special test account - Sandbox Apple ID, and ensure the testing device is added to the Developer Account in the App Store Connect. Sandbox testing is ideal for developers who wish to personally test purchases on a device connected to their Mac via XCode. For more details, you can refer to the [Apple's documentation on Testing in-app purchases with sandbox](https://developer.apple.com/documentation/storekit/in-app_purchase/testing_in-app_purchases_with_sandbox). :::warning Test on a real device To validate the end-to-end purchase process, it's essential to conduct testing on a real device. While testing on a simulator allows you to examine paywalls, it does not enable interaction with the Apple server, making it impossible to test purchases. ::: ## Before you start testing Before you start testing in-app purchases, make sure that: 1. Your Apple Developer Program account is active. For more information, see Apple's [What you need to enroll](https://developer.apple.com/programs/enroll). 2. Your membership Account Holder has signed the Paid Applications Agreement, as described in Apple's [Sign and update agreements](https://developer.apple.com/help/app-store-connect/manage-agreements/sign-and-update-agreements). 3. You set up the product information in App Store Connect for the app you’re testing. At a minimum, set up a product reference name, product ID, a localized name, and a price. 4. The **Keychain Sharing** capability is disabled. For more information, see Apple's article [Configuring keychain sharing](https://developer.apple.com/documentation/xcode/configuring-keychain-sharing). 5. You’re running a development-signed rather than a production-signed build of your app. 6. You have completed all the steps outlined in the [release checklist](release-checklist). ## Prepare for Sandbox testing Testing in-app purchases in the sandbox environment doesn’t involve uploading your app binary to App Store Connect. Instead, you build and run your app directly from Xcode. However, it does require a special test account - Sandbox Apple ID. ### Step 1. Create a Sandbox test account (Sandbox Apple ID) in the App Store Connect :::warning When testing purchases, it's important to create a new sandbox test account each time. This helps keep the purchase history clean, ensuring better performance and smoother functionality. Alternatively, you can clear the purchase history for your existing test account. For more details, check out the [Apple Developer documentation](https://developer.apple.com/documentation/storekit/in-app_purchase/testing_in-app_purchases_with_sandbox/#3894622). ::: To create a Sandbox Apple ID: 1. Open **App Store Connect**. Proceed to [**Users and Access** → **Sandbox** → **Test Accounts**](https://appstoreconnect.apple.com/access/users/sandbox) section. 2. Click the add button **(+)** button next to the **Test Accounts** title. 3. In the **New Tester** window, enter the data of the test user. :::warning - Make sure to provide a valid email you can verify. - Make sure to define the **Country or Region** which you plan to test. ::: 4. Click the **Create** button to confirm the creation ### Step 3. Add the Sandbox test account to your device The first time you run an app from XCode on your device, there's no need to manually add a Sandbox account. Upon building the app in XCode and running it on your device, when you initiate a purchase, the device prompts you to enter the Apple ID for the purchase. Simply enter your Sandbox Apple ID and password at this juncture, and the Sandbox test account will be automatically added to your device. If you need to change the Sandbox Apple ID associated with your device, you can do so directly on the device by following these steps: 1. On iOS 12, navigate to **Settings > [Your Account] > App Store > Sandbox Account**. On iOS 13 or greater, navigate to **Settings > App Store > Sandbox Account**. 2. Tap the current Sandbox Apple ID in the **Sandbox Account** section. 3. Tap the **Sign Out** button. 4. Tap the **Sign In** button. 5. In the **Use the Apple ID for Apple Media Services** window, tap the **Use Other Apple ID** button. 6. In the **Apple ID Sign-In Requested** window, enter the new sandbox account credentials that you previously created. 7. Tap the **Done** button. 8. In the **Apple ID Security** window, tap the **Other options** button. 9. In the **Protect your account** window, tap the **Do not upgrade** button. The added sandbox account is shown in the **Sandbox Account** section of your iOS device **Settings**. ### Step 4. Connect the device to your Mac with XCode To execute the built app version on your real device, include the device as a run destination in the Xcode project 1. Connect your real device to the Mac with XCode using a cable or using the same Wi-Fi. 2. Choose the **Windows** -> **Devices and Simulators** from the XCode main menu. 3. In the **Devices** tab, choose your device. 4. Tap the **Trust** button on your mobile phone. Your device is connected to the XCode and can be used for sandbox testing. ### Step 5. Build the app and run it Click the **Run** button in the toolbar or choose **Product -> Run** to build and run the app on the connected real device. If the build is successful, Xcode runs the app on your iOS device and opens a debugging session in the debug area of the XCode. The app is ready for testing on the device. :::note When you’re done testing the app, click the **Stop** button in the XCode toolbar. ::: ### Step 6. Make purchase Make a purchase in your mobile app via paywall. :::info Now you can [validate that the test purchase is successful](validate-test-purchases). ::: ## Subscription renewal, billing retry, and grace period in Apple Sandbox Keep in mind that in the Apple Sandbox environment, subscription renewals happen faster, and both the billing retry and grace periods are shorter than in production. The default values are shown in the table below. You can adjust a tester’s subscription renewal rate, billing retry period, and grace period at any time. For more details, refer to the [official Apple documentation](https://developer.apple.com/help/app-store-connect/test-in-app-purchases/manage-sandbox-apple-account-settings/#edit-subscription-renewal-speed). | Subscription duration | 1 week | 1 month | 2 months | 3 months | 6 months | 1 year | | :----------------------------- | :--------- | :--------- | :--------- | :--------- | :--------- | :--------- | | Subscription renewal speed | 3 minutes | 5 minutes | 10 minutes | 15 minutes | 30 minutes | 1 hour | | Length of Billing Retry | 10 minutes | 10 minutes | 10 minutes | 10 minutes | 10 minutes | 10 minutes | | Length of Billing Grace Period | 3 minutes | 5 minutes | 5 minutes | 5 minutes | 5 minutes | 5 minutes | Additionally, keep in mind that in the sandbox environment, auto-renewable subscriptions renew up to 12 times before they stop. --- # File: test-purchases-with-testflight.md --- --- title: "Test in-app purchases with TestFlight" description: "Learn how to test in-app purchases with TestFlight using Adapty for a smooth testing process." --- TestFlight lets your team test your app on real devices and provide feedback. Testers use their real Apple accounts, and all testing is conducted in a sandbox environment. This means transactions and purchases won't incur charges. Testing with TestFlight in the sandbox environment is ideal for team-based testing. For more information, check out [Apple's documentation on Beta testing with TestFlight](https://developer.apple.com/testflight/). :::warning Test on a real device To validate the complete purchase process, testing on a real device is crucial. While simulators help examine paywalls, they can't interact with Apple's servers, so you won't be able to test purchases. ::: ## Before you start testing Ensure the following prerequisites are met before testing in-app purchases: 1. Your Apple Developer Program account is active. Learn more in [Apple's guide on enrolling](https://developer.apple.com/programs/enroll). 2. Your Account Holder has signed the Paid Applications Agreement. Details are available in [Apple's guide on managing agreements](https://developer.apple.com/help/app-store-connect/manage-agreements/sign-and-update-agreements). 3. Product information is set up in App Store Connect for the app you're testing, including a product reference name, product ID, localized name, and price. 4. The **Keychain Sharing** capability is disabled. Learn more in [Apple's guide on configuring keychain sharing](https://developer.apple.com/documentation/xcode/configuring-keychain-sharing). 5. You're using a development-signed build of your app, not a production-signed one. 6. All steps in the [release checklist](release-checklist) are complete. 7. You’ve reviewed how subscription renewals work in TestFlight. Renewals are accelerated: subscriptions renew daily (up to 6 times within a week), regardless of the subscription's duration. See the [official Apple documentation](https://developer.apple.com/help/app-store-connect/test-a-beta-version/subscription-renewal-rate-in-testflight) for details. ## Prepare for testing with TestFlight When testing purchases in TestFlight, always use a real device and your actual Apple account. Transactions won't incur charges when using a development-signed build. Follow these steps to test beta versions of your app with TestFlight: 1. Build your mobile app and send it to TestFlight via App Store Connect without releasing it. 2. Install the [TestFlight app](https://itunes.apple.com/us/app/testflight/id899247664?mt=8) on your iOS device. 3. Share the link to your app with testers, and open the link on the test device. 4. If you're a new tester, tap **Accept** to join. 5. Tap **Install** to install the app on your device. Your app is now installed and ready for testing. ## Make purchase Use a paywall in your app to make a test purchase. Once the purchase is complete, [validate the test purchase](validate-test-purchases) to confirm it was successful. ## Subscriptions in TestFlight Keep in mind that in TestFlight, subscription renewals happen daily, regardless of the subscription's actual duration. Each subscription can renew up to six times within one week. For more details, refer to the [official Apple documentation](https://developer.apple.com/help/app-store-connect/test-a-beta-version/subscription-renewal-rate-in-testflight). | Production subscription period | Sandbox subscription renewal | TestFlight subscription renewal | | ------------------------------ | ---------------------------- | ------------------------------- | | 3 days | 2 minutes | 1 day | | 1 week | 3 minutes | 1 day | | 1 month | 5 minutes | 1 day | | 2 months | 10 minutes | 1 day | | 3 months | 15 minutes | 1 day | | 6 months | 30 minutes | 1 day | | 1 year | 1 hour | 1 day | :::note **Example scenario:** If you start a 1-month subscription on February 1st, it'll renew every 24 hours for a total of 6 renewals before being canceled. Since subscriptions renew at an accelerated rate in TestFlight, you'll see a new transaction for each renewal on February 2nd, 3rd, 4th, 5th, 6th, and 7th. The subscription's auto-renewal will then be disabled on February 8th. ::: --- # File: test-webhook.md --- --- title: "Test webhook integration" description: "Test webhook integrations in Adapty to automate subscription event tracking." --- After you set up your integration, it's time to test it. You can test both your sandbox and production integration. We recommend starting with the sandbox one and validating the maximum on it: - The events are sent and successfully delivered. - You set up the options correctly for historical events, subscription price for the **Trial started** event, attribution, user attributes, and Google Play Store purchase token to be sent or not sent with an event. - You mapped event names correctly and your server can process them. ## How to test Before you start testing an integration, make sure you have already: 1. Set up the webhook integration as described in the [Set up webhook integration](set-up-webhook-integration) topic. 2. Set up the environment as described in the [Test in-app purchases in Apple App Store](testing-purchases-ios) and [Test in-app purchases in Google Play Store](testing-on-android) topics. Make sure you built your test app in the sandbox environment rather than in the production one. 3. Make a purchase/start a trial/make a refund that will raise an event you've chosen to send to the webhook. For example, to get the **Subscription started** event, purchase a new subscription. ## Validation of the result ### Successful sending events result In case of successful integration, an event will appear in the **Last sent events** section of the integration and will have the **Success** status. ### Unsuccessful sending events result | Issue | Solution | |-----|--------| | The event did not appear | Your purchase did not occur and therefore the event was not created. Refer to the [Troubleshooting test purchases](troubleshooting-test-purchases) topic for the solution. | | The event appeared and has the **Sending failed** status |We determine the deliverability based on HTTP status and consider everything **outside the 200-399 range** to be a fail.
To find more on the issue, hover over the **Sending failed** status of your unsuccessful event as shown below.
| --- # File: testing-on-android.md --- --- title: "Test in-app purchases in Google Play Store" description: "Test subscription purchases on Android using Adapty." --- Testing in-app purchases (IAPs) in your Android app can be a crucial step before releasing your app to the public. Sandbox testing is a safe and efficient way to test IAPs without charging real money to your users. In this guide, we'll walk you through the process of sandbox testing IAPs on the Google Play Store for Android. ## Test your app on a real device To ensure optimal performance of your Android app, it's recommended that you test it on a real device instead of an emulator. While we have successfully tested on emulators, Google recommends using a real device. If you do decide to use an emulator, make sure that it has Google Play installed. This will help ensure that your app is functioning properly. ## Create a test user for app testing To facilitate testing during later stages of development, you'll need to create a test user. This user will be the first account you log in with on your Android testing device. Note that the primary account on an Android device can only be changed by performing a factory reset. Therefore, it's important to create a separate test user account to avoid having to perform a factory reset on your device. ## Configure licence testing for your app Once you've created a test user account, you'll need to configure licensing testing for your app. To do this, follow these steps: 1. In the Console sidebar, navigate to **Setup**. 2. Select **License testing**. 3. Add the account that you're using on your real device (i.e., the account you're currently logged in with) to the list. This will allow you to configure licensing testing for your app and ensure that it's functioning properly. In our example, we already have a list of testers: ## Create a closed track and add the test user to it 1. Publish a signed version of your app to a closed track. If you haven't created a closed track yet, you can create one in the **Closed testing** section of the **Testing** menu. Just as previously, you can use one of the existing lists or create a new one: 2. Press **Enter**, and click the **Save changes** button. 3. Open the **Opt-in URL** in your testing device to make the user a tester. You can send the URL to your device via email, for example. :::warning Important Opening the opt-in URL marks your Play account for testing. If you don't complete this step, products will not load. ::: :::warning Check Your Application ID Often developers will use a different application ID for their test builds. This will cause you problems since Google Play Services uses the application ID to find your in-app purchases. ::: :::warning Add a PIN to the test device if needed There are cases where a test user may be allowed to purchase consumables, but not subscriptions, if the test device does not have a PIN. This may manifest in a cryptic "Something went wrong" message. Make sure that the test device has a PIN, and that the device is logged into Google Play Store. ::: ## Upload a signed APK to the closed track Generate a signed APK or use Android App Bundle to upload a signed APK to the closed track you just created. You don't even need to roll out the release. Just upload the APK. You can find more information about this in this support article. > 🌎 Make your release available in at least one country > > If your app is new, you may need to make it available in your country or region. To do so, go to Testing > Closed testing, click on your test track, and go to Countries/regions to add the desired countries and regions. ## Test in-app purchases After you've uploaded the APK, wait a few minutes for the release to process. Then, open your testing device and sign in with the email account you added to the Testers list. You can then test in-app purchases as you would on a production app. If you run into any issues, refer to the documentation or contact Google Play Developer support. --- # File: testing-purchases-ios.md --- --- title: "Test in-app purchases in Apple App Store" description: "Learn how to test in-app purchases on iOS with Adapty’s sandbox mode." --- Once you've configured everything in the Adapty Dashboard and your mobile app, it's time to conduct in-app purchase testing. :::warning Test on a real device Whatever tool you choose, it's essential to conduct testing on a real device to validate the end-to-end purchase process. While testing on a simulator allows you to examine paywalls, it does not enable interaction with the Apple's servers, making it impossible to test purchases. ::: **Note:** none of the test tools charge users when they test buying a product. The App Store doesn’t send emails for purchases or refunds made in the test environments. You can either choose to test in Sandbox or using TestFlight. Sandbox is the best choice when you as a developer want to test the purchases yourself on a device linked to your Mac with XCode, while TestFlight is more convenient for other members of the team. Choose the method that works best for you: - [Testing in Sandbox](test-purchases-in-sandbox) - [Testing via TestFlight](test-purchases-with-testflight) --- # File: trials-renewal-cancelled.md --- --- title: "Trials renewal cancelled" description: "Understand trial renewals, cancellations, and subscription flows with Adapty’s insights." --- The Trials renewal cancelled chart displays 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 a paid subscription, yet the user still has premium features of the app until the end of the current period. ### Calculation Adapty calculates the number of Trials renewal cancelled during a specific period by counting the trials that users have cancelled before the end of their trial period. This count remains unchanged regardless of the trial's auto-renewal status. ### 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 the[ this documentation.](controls-filters-grouping-compare-proceeds) ### Trials renewal cancelled chart usage The Trials renewal cancelled chart is a useful tool that provides insights into the trial cancellation patterns of your app's users. By monitoring the number of users who have cancelled their trial subscriptions during a specific period, you can optimize your app's trial offerings and enhance user satisfaction. By leveraging the insights provided by this chart, you can determine whether your trial strategy needs adjustment to encourage users to continue their subscriptions. ### Similar metrics In addition to the Trials renewal cancelled 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) - [Expired trials](expired-churned-trials) --- # File: troubleshooting-test-purchases.md --- --- title: "Troubleshooting test purchases" description: "Troubleshoot test purchases in Adapty and resolve common in-app transaction issues." --- If you encounter transaction issues, please first make sure you have completed all the steps outlined in the [release checklist](release-checklist). If you've completed all the steps and still encounter issues, please follow the guidance provided below to resolve them: ## An error is returned in the mobile app Refer to the error list for your platform: [for iOS](ios-sdk-error-handling), [for Android](android-sdk-error-handling), [for Flutter](error-handling-on-flutter-react-native-unity), [React Native](react-native-troubleshoot-purchases.md), and [Unity](unity-troubleshoot-purchases.md) and follow our recommendations to resolve the issue. ## Transaction is absent from the Event Feed although no error is returned in the mobile app To resolve this issue, please check the following: 1. **For iOS**: Ensure you use a real device rather than a simulator. 2. Ensure that the `Bundle ID`/`Package name` of your app matches the one in the [**App settings**](https://app.adapty.io/settings/general). 3. Ensure the `PUBLIC_SDK_KEY` in your app matches the **Public SDK key** in the Adapty Dashboard: [**App settings**-> **General** tab -> **API keys** subsection](https://app.adapty.io/settings/general). ## No event is present in my testing profile This is normal behavior. A new user profile record is automatically created in Adapty when: - A user runs your app for the first time - A user logs out of your app **Why this happens:** All transactions and events are tied to the profile that generated the first transaction. This keeps the entire transaction history (trials, purchases, renewals) linked to the same profile. **What you'll see:** New profile records (called "non-original profiles") may appear without events but will retain access levels. You may see `access_level_updated` events. This is expected behavior. **For testing:** To avoid multiple profiles, create a new test account (Sandbox Apple ID) each time you reinstall the app. For more details, see [Profile record creation](profiles-crm#profile-record-creation). 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. ## Prices do not reflect the actual prices set in App Store Connect In both Sandbox and TestFlight which uses the sandbox environment for in-app purchases, it's important to verify that the purchase flow functions correctly, rather than focusing on the accuracy of prices. It's worth noting that Apple's API can occasionally provide inaccurate data, particularly when different regions are configured for devices or accounts. And since the prices come directly from the Store and the Adapty backend does not affect purchase prices in any way, you can ignore any inaccuracy in prices during the testing of the purchases through Adapty. Therefore, prioritize testing the purchase flow itself over the accuracy of prices to ensure it functions as intended. ## The transaction time in the Event Feed is incorrect The **Event Feed** uses the time zone set in the **App Settings**. To align the time zone of events with your local time, adjust the **Reporting timezone** in [**App settings** -> **General** tab](https://app.adapty.io/settings/general). ## Paywalls and products take a long time to load This issue can occur if your test account has a long transaction history. We highly recommend creating a new test account each time, as outlined in our [Create a Sandbox Test Account (Sandbox Apple ID) in App Store Connect](test-purchases-in-sandbox#step-1-create-a-sandbox-test-account--sandbox-apple-id-in-the-app-store-connect) section. If you're unable to create a new account, you can clear the transaction history on your current account by following these steps on your iOS device: 1. Open **Settings** and tap **App Store**. 2. Tap your **Sandbox Apple ID**. 3. In the popup, select **Manage**. 4. On the **Account Settings** page, tap **Clear Purchase History**. For more details, check out the [Apple Developer documentation](https://developer.apple.com/documentation/storekit/in-app_purchase/testing_in-app_purchases_with_sandbox/#3894622). --- # File: unity-check-subscription-status.md --- --- title: "Check subscription status in Unity SDK" description: "Learn how to check subscription status in your Unity app with Adapty." displayed_sidebar: sdkunity --- 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: ```csharp showLineNumbers Adapty.GetProfile((profile, error) => { if (error != null) { // handle the error return; } // check the access }); ``` ### Listen to subscription updates To automatically receive profile updates in your app: 1. Extend `AdaptyEventListener` and implement the `OnLoadLatestProfile` method - 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. ```csharp public class SubscriptionManager : MonoBehaviour, AdaptyEventListener { private AdaptyProfile currentProfile; void Start() { // Register this object as an Adapty event listener Adapty.AddEventListener(this); } // Store the profile when it updates public void OnLoadLatestProfile(AdaptyProfile profile) { currentProfile = profile; // Update UI, unlock content, etc. } // Use stored profile instead of calling getProfile() public bool HasAccess() { if (currentProfile?.AccessLevels != null && currentProfile.AccessLevels.ContainsKey("premium")) { return currentProfile.AccessLevels["premium"].IsActive; } return false; } } ``` :::note Adapty automatically calls `OnLoadLatestProfile` 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. ```csharp private void CheckAccessLevel() { Adapty.GetProfile((profile, error) => { if (error != null) { Debug.LogError("Error checking access level: " + error.Message); // Show paywall if access check fails return; } var accessLevel = profile.AccessLevels["YOUR_ACCESS_LEVEL"]; if (accessLevel == null || !accessLevel.IsActive) { // Show paywall if no access } }); } private void InitializePaywall() { LoadPaywall(); CheckAccessLevel(); } ``` ## Next steps Now, when you know how to track the subscription status, learn how to [work with user profiles](unity-quickstart-identify.md) to ensure they can access what they have paid for. --- # File: unity-deal-with-att.md --- --- title: "Deal with ATT in Unity SDK" description: "Get started with Adapty on Unity to streamline subscription setup and management." displayed_sidebar: sdkunity --- 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. ```csharp showLineNumbers var builder = new Adapty.ProfileParameters.Builder() .SetAppTrackingTransparencyStatus(IOSAppTrackingTransparencyStatus.Authorized); Adapty.UpdateProfile(builder.Build(), (error) => { if(error != null) { // 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: unity-display-legacy-pb-paywalls.md --- --- title: "Display legacy Paywall Builder paywalls in Unity SDK" description: "Learn how to display legacy Paywall Builder paywalls in your Unity app with Adapty SDK." displayed_sidebar: sdkunity --- This page contains guides for displaying legacy Paywall Builder paywalls in your Unity app. Choose the topic you need: - **[Fetch legacy Paywall Builder paywalls](unity-get-legacy-pb-paywalls)** - Retrieve legacy paywalls and their configuration - **[Present legacy Paywall Builder paywalls](unity-present-paywalls-legacy)** - Display legacy paywalls to users - **[Handle legacy paywall events](unity-handling-events-legacy)** - Manage legacy paywall interactions - **[Hide legacy Paywall Builder paywalls](unity-hide-legacy-paywall-builder-paywalls)** - Hide legacy paywalls --- # File: unity-get-legacy-pb-paywalls.md --- --- title: "Fetch legacy Paywall Builder paywalls in Unity SDK" description: "Retrieve legacy PB paywalls in your Unity app with Adapty SDK." displayed_sidebar: sdkunity --- 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 Unity 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](unity-get-pb-paywalls). - For fetching **Remote config paywalls**, see [Fetch paywalls and products for remote config paywalls](fetch-paywalls-and-products-unity). :::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`](unity-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-unity). For Unity, the view configuration is automatically handled when you present the paywall using the `AdaptyUI.ShowPaywall()` method. --- # File: unity-get-pb-paywalls.md --- --- title: "Fetch Paywall Builder paywalls and their configuration in Unity SDK" description: "Learn how to retrieve PB paywalls in Adapty for better subscription control in your Unity app." displayed_sidebar: sdkunity --- 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 Unity 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](unity-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-unity) 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. :::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`](unity-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-unity). In Unity SDK, directly call the `CreateView` method without manually fetching the view configuration first. :::warning The result of the `CreateView` method can only be used once. If you need to use it again, call the `CreateView` method anew. Calling it twice without recreating may result in the `AdaptyUIError.viewAlreadyPresented` error. ::: ```csharp showLineNumbers var parameters = new AdaptyUICreateViewParameters() .SetPreloadProducts(preloadProducts) .SetLoadTimeout(new TimeSpan(0, 0, 3)); AdaptyUI.CreateView(paywall, parameters, (view, error) => { // handle the result }); ``` 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. | | **PreloadProducts** | 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. | | **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. | | **CustomTimers** | optional | Define a dictionary of custom timers and their end dates. Custom timers allow you to display countdown timers in your paywall. | :::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. ## Set up developer-defined timers To use custom timers in your Unity app, you can pass a dictionary of timer IDs and their end dates directly to the `SetCustomTimers` method. Here is an example: ```csharp showLineNumbers var customTimers = new DictionaryThis 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.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 Unity]( sdk-installation-unity#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-unity) 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-unity). | | 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-unity#configure-adapty-sdk) using the `Adapty.activate` method. | | badRequest | 2003 | Bad request.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 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`.
| ### Use Adapty Unity Plugin 1. Create a script to listen to Adapty events. Name it `AdaptyListener` in your scene. We suggest using the `DontDestroyOnLoad` method for this object to ensure it persists throughout the application's lifespan. Adapty uses `AdaptySDK` namespace. At the top of your script files that use the Adapty SDK, you may add ```csharp showLineNumbers title="C#" using AdaptySDK; ``` 2. Subscribe to Adapty events: ```csharp showLineNumbers title="C#" using UnityEngine; using AdaptySDK; public class AdaptyListener : MonoBehaviour, AdaptyEventListener { void Start() { DontDestroyOnLoad(this.gameObject); Adapty.SetEventListener(this); } public void OnLoadLatestProfile(Adapty.Profile profile) { // handle updated profile data } } ``` 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: unity-legacy.md --- --- title: "Legacy guides for Unity SDK" description: "Legacy documentation for Adapty Unity SDK." displayed_sidebar: sdkunity --- This page contains legacy documentation for Adapty Unity SDK. Choose the topic you need: - **[Legacy installation guide](unity-legacy-install)** - Install and configure legacy Unity SDK - **[Display legacy Paywall Builder paywalls](unity-display-legacy-pb-paywalls)** - Work with legacy paywall builder --- # File: unity-listen-subscription-changes.md --- --- title: "Check subscription status in Unity SDK" description: "Track and manage user subscription status in Adapty for improved customer retention in your Unity 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).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: ```csharp showLineNumbers Adapty.GetProfile((profile, error) => { if (error != null) { // handle the error return; } // "premium" is an identifier of default access level var accessLevel = profile.AccessLevels["premium"]; if (accessLevel != null && accessLevel.IsActive) { // grant access to premium features } }); ``` ### Listening for subscription status updates Whenever the user's subscription changes, Adapty fires an event. To receive messages from Adapty, you need to make some additional configuration: ```csharp showLineNumbers // Extend `AdaptyEventListener ` with `OnLoadLatestProfile ` method: public class AdaptyListener : MonoBehaviour, AdaptyEventListener { public void OnLoadLatestProfile(AdaptyProfile 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: unity-localizations-and-locale-codes.md --- --- title: "Use localizations and locale codes in Unity SDK" description: "Learn how to localize paywalls in your Unity app with Adapty SDK." --- ## 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: ```csharp showLineNumbers // 1. Modify your localization files (e.g., using Unity's Localization package) /* 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 using UnityEngine; using UnityEngine.Localization; using UnityEngine.Localization.Settings; using AdaptySDK; public class PaywallManager : MonoBehaviour { public async void FetchPaywall() { // Get the current locale from Unity's Localization system var locale = LocalizationSettings.SelectedLocale; var localeCode = GetAdaptyLocaleCode(locale); // Pass locale code to Adapty.GetPaywall or Adapty.GetPaywallForDefaultAudience method Adapty.GetPaywall("placement_id", localeCode, (paywall, error) => { if (error != null) { // handle the error return; } // Use the paywall }); } private string GetAdaptyLocaleCode(Locale locale) { // Convert Unity locale to Adapty format var localeIdentifier = locale.Identifier.Code; return localeIdentifier.ToLower().Replace('_', '-'); } } ``` 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: ```csharp showLineNumbers using UnityEngine; using System.Globalization; using AdaptySDK; public class PaywallManager : MonoBehaviour { public void FetchPaywall() { var localeCode = GetSystemLocaleCode(); // Pass locale code to Adapty.GetPaywall or Adapty.GetPaywallForDefaultAudience method Adapty.GetPaywall("placement_id", localeCode, (paywall, error) => { if (error != null) { // handle the error return; } // Use the paywall }); } private string GetSystemLocaleCode() { // Get the system's current culture var culture = CultureInfo.CurrentCulture; var languageCode = culture.TwoLetterISOLanguageName; var regionCode = culture.Name.Contains('-') ? culture.Name.Split('-')[1] : null; if (!string.IsNullOrEmpty(regionCode)) { return $"{languageCode}-{regionCode.ToLower()}"; } return languageCode; } } ``` 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: unity-making-purchases.md --- --- title: "Make purchases in mobile app in Unity 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-unity#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. ::: ```csharp showLineNumbers using AdaptySDK; void MakePurchase(AdaptyPaywallProduct product) { 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 successfull purchase break; default: break; } }); } ``` 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: ```csharp showLineNumbers Adapty.MakePurchase(product, subscriptionUpdateParams, (profile, error) => { if(error != null) { // Handle the error return; } // successful cross-grade }); ``` 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 SDK method: ```csharp showLineNumbers Adapty.PresentCodeRedemptionSheet((error) => { // handle the error }); ``` :::danger Based on our observations, the Offer Code Redemption sheet in some apps may not work reliably. We recommend redirecting the user directly to the App Store. In order to do this, you need to open the url of the following format: `https://apps.apple.com/redeem?ctx=offercodes&id={apple_app_id}&code={code}` ::: --- # File: unity-migration-guide.md --- --- title: "SDK migration guide" description: "Migration guides for Unity Adapty SDK." slug: /unity-migration-guide displayed_sidebar: sdkunity --- ## Migration Guides ### [Migration guide to Unity Adapty SDK 3.x](https://docs.adapty.io/unity/migration-guide) Learn how to migrate from older versions to Unity Adapty SDK 3.x. ## What's New ### Version 3.x - Enhanced paywall presentation - Improved error handling - Better C# support - Performance optimizations ### Version 2.x - New onboarding features - Enhanced analytics - Improved purchase flow - Bug fixes and stability improvements ## Breaking Changes ### Version 3.x - Updated observer API - Changed paywall presentation methods - Modified error handling structure ### Version 2.x - Updated onboarding API - Changed profile structure - Modified purchase flow ## Migration Checklist When migrating to a new version: - [ ] Review breaking changes - [ ] Update API calls - [ ] Test all functionality - [ ] Update error handling - [ ] Verify analytics tracking - [ ] Test on all platforms --- # File: unity-paywalls.md --- --- title: "Paywalls in Unity SDK" description: "Learn how to work with paywalls in your Unity app with Adapty SDK." displayed_sidebar: sdkunity --- This page contains all guides for working with paywalls in your Unity app. Choose the topic you need: - **[Get paywalls](unity-get-pb-paywalls)** - Retrieve paywalls from Adapty - **[Display paywalls](unity-present-paywalls)** - Present paywalls to users - **[Handle paywall events](unity-handling-events)** - Manage paywall interactions - **[Work with paywalls offline](unity-use-fallback-paywalls)** - Use fallback paywalls when offline - **[Localize paywalls](unity-localizations-and-locale-codes)** - Support multiple languages - **[Implement paywalls manually](unity-implement-paywalls-manually)** - Build custom paywall UI --- # File: unity-present-paywalls-legacy.md --- --- title: "Present legacy Paywall Builder paywalls in Unity SDK" description: "" --- 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 [new Paywall Builder](adapty-paywall-builder) requires Adapty SDK 3.0 or later, which is currently not available for Unity. 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. ::: ```csharp showLineNumbers title="Unity" view.Present((error) => { // handle the error }); ``` --- # File: unity-present-paywalls.md --- --- title: "Display paywalls" description: "Learn how to display paywalls in your Unity app with Adapty SDK." slug: /unity-present-paywalls displayed_sidebar: sdkunity --- 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 **new Paywall Builder**, which requires Adapty SDK 3.3.0 or later. If you're using the legacy Paywall Builder (compatible with Adapty SDK version 2.x and earlier), check out [Present legacy Paywall Builder paywalls in Unity](unity-present-paywalls-legacy). To present remote config paywalls, see [Render paywalls designed with remote config](present-remote-config-paywalls). ::: To display a paywall, use the `view.Present()` method on the `view` created by the `CreateView` method. Each `view` can only be used once. If you need to display the paywall again, call `createView` one more to create a new `view` instance. :::warning Reusing the same `view` without recreating it may result in an `AdaptyUIError.viewAlreadyPresented` error. ::: ```csharp showLineNumbers title="Unity" view.Present((error) => { // handle the error }); ``` :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: --- # File: unity-quickstart-identify.md --- --- title: "Identify users in Unity SDK" description: "Quickstart guide to setting up Adapty for in-app subscription management in Unity." --- :::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. ::: ```csharp showLineNumbers Adapty.Identify("YOUR_USER_ID", (error) => { // Unique for each user 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). ::: ```csharp showLineNumbers using UnityEngine; using AdaptySDK; var builder = new AdaptyConfiguration.Builder("YOUR_API_KEY") .SetCustomerUserId("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(builder.Build(), (error) => { if (error != null) { // handle the error return; } }); ``` ### 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. ::: ```csharp 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`](unity-check-subscription-status.md) right after the identification, or [listen for profile updates](unity-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**](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**](unity-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: unity-quickstart-paywalls.md --- --- title: "Enable purchases by using paywalls in Unity SDK" description: "Learn how to present paywalls in your Unity app with Adapty SDK." slug: /unity-quickstart-paywalls displayed_sidebar: sdkunity --- 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](unity-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](unity-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 `CreateView` 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. ::: ```csharp showLineNumbers Adapty.GetPaywall("YOUR_PLACEMENT_ID", (paywall, error) => { if(error != null) { // handle the error return; } // paywall - the resulting object }); var parameters = new AdaptyUICreateViewParameters() AdaptyUI.CreateView(paywall, parameters, (view, error) => { // handle the result }); ``` :::info This quickstart provides the minimum configuration required to display a paywall. For advanced configuration details, see our [guide on getting paywalls](unity-get-pb-paywalls). ::: ## 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 `СreateView` 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. ```csharp showLineNumbers title="Unity" view.Present((error) => { // handle the error }); ``` :::info For more details on how to display a paywall, see our [guide](unity-present-paywalls.md). ::: ## 3. Handle button actions When users click buttons in the paywall, the Unity SDK automatically handles purchases and restoration. However, other buttons have custom or pre-defined IDs and require handling actions in your code. 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](unity-handle-paywall-actions.md) and [events](unity-handling-events.md). ::: ```csharp showLineNumbers title="Unity" public void PaywallViewDidPerformAction( AdaptyUIView view, AdaptyUIUserAction action ) { switch (action.Type) { case AdaptyUIUserActionType.Close: view.Dismiss(null); break; case AdaptyUIUserActionType.OpenUrl: // Open the URL using Unity's Application.OpenURL Application.OpenURL(action.Value); break; default: break; } } ``` ## Next steps Your paywall is ready to be displayed in the app. Now, you need to [check the users' access level](unity-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. ```csharp using System; using UnityEngine; public class PaywallManager : MonoBehaviour { [SerializeField] private string placementId = "YOUR_PLACEMENT_ID"; private AdaptyUIView currentPaywallView; void Start() { GetAndDisplayPaywall(); } private void GetAndDisplayPaywall() { Adapty.GetPaywall(placementId, (paywall, error) => { if (error != null) { Debug.LogError("Error getting paywall: " + error.Message); return; } if (paywall.hasViewConfiguration) { CreateAndPresentPaywallView(paywall); } else { Debug.LogWarning("Paywall was not created using the builder"); } }); } private void CreateAndPresentPaywallView(AdaptyPaywall paywall) { var parameters = new AdaptyUICreateViewParameters(); AdaptyUI.CreateView(paywall, parameters, (view, error) => { if (error != null) { Debug.LogError("Error creating paywall view: " + error.Message); return; } currentPaywallView = view; view.Present((presentError) => { if (presentError != null) { Debug.LogError("Error presenting paywall: " + presentError.Message); return; } Debug.Log("Paywall presented successfully"); }); }); } public void PaywallViewDidPerformAction( AdaptyUIView view, AdaptyUIUserAction action ) { switch (action.Type) { case AdaptyUIUserActionType.Close: Debug.Log("Close button pressed"); view.Dismiss(null); break; case AdaptyUIUserActionType.OpenUrl: Application.OpenURL(action.Value); break; default: break; } } public void ShowPaywall() { GetAndDisplayPaywall(); } void OnDestroy() { if (currentPaywallView != null) { currentPaywallView.Dismiss(null); } } } ``` --- # File: unity-reference.md --- --- title: "Reference for Unity SDK" description: "Reference documentation for Adapty Unity SDK." displayed_sidebar: sdkunity --- This page contains reference documentation for Adapty Unity SDK. Choose the topic you need: - **[SDK models](unity-sdk-models)** - Data models and structures used by the SDK - **[Handle errors](unity-handle-errors)** - Error handling and troubleshooting - **[Unity SDK reference](https://github.com/adaptyteam/AdaptySDK-Unity)** - Complete API documentation --- # File: unity-restore-purchase.md --- --- title: "Restore purchases in mobile app in Unity 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: ```csharp showLineNumbers Adapty.RestorePurchases((profile, error) => { if (error != null) { // handle the error return; } var accessLevel = profile.AccessLevels["YOUR_ACCESS_LEVEL"]; if (accessLevel != null && accessLevel.IsActive) { // restore access } }); ``` 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: unity-sdk-migration-guides.md --- --- title: "Unity SDK Migration Guides" description: "Migration guides for Adapty Unity SDK versions." --- This page contains all migration guides for Adapty Unity SDK. Choose the version you want to migrate to for detailed instructions: - **[Migrate to v. 3.4](migration-to-unity-sdk-34)** - **[Migrate to v. 3.3](migration-to-unity330)** - **[Migrate to v. 3.0](migration-to-unity-sdk-v3)** --- # File: unity-sdk-models.md --- --- title: "Unity SDK Models" description: "Data models and types for Unity Adapty SDK." slug: /unity-sdk-models displayed_sidebar: sdkunity --- ## Interfaces ### AdaptyPaywallProduct An information about a [product.](https://swift.adapty.io/documentation/adapty/adaptypaywallproduct) | Name | Type | Description | |:-----------------------------------------|:------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | VendorProductId | string | Unique identifier of the product | | AdaptyProductId | string | Unique identifier of the product in Adapty | | PaywallVariationId | string | The identifier of the variation, used to attribute purchases to the paywall | | PaywallABTestName | string | Parent A/B test name | | PaywallName | string | Parent paywall name | | LocalizedDescription | string | A description of the product | | LocalizedTitle | string | The name of the product | | IsFamilyShareable | bool | Indicates whether the product is available for family sharing in App Store Connect | | RegionCode | string (optional) | Product locale region code | | Price | [AdaptyPrice](#adaptyprice) | The object which represents the main price for the product | | Subscription | [AdaptySubscription](#adaptysubscription) (optional) | Detailed information about subscription (intro, offers, etc.) | ### AdaptyPrice | Name | Type | Description | | :---------------- | :--------------- | :------------------------------------------------------------------------------------------------------------------------------- | | Amount | double | Discount price of a product in a local currency | | CurrencyCode | string (optional) | The currency code of the locale used to format the price of the product | | CurrencySymbol | string (optional) | The currency symbol of the locale used to format the price of the product | | LocalizedString | string (optional) | A formatted price of a discount for a user's locale | ### AdaptySubscription | Name | Type | Description | |:------------------------------|:------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | GroupIdentifier | string (optional) | The identifier of the subscription group to which the subscription belongs | | Period | [AdaptySubscriptionPeriod](#adaptysubscriptionperiod) | A ProductSubscriptionPeriodModel object. The period details for products that are subscriptions | | LocalizedPeriod | string (optional) | Localized subscription period of the product | | Offer | [AdaptySubscriptionOffer](#adaptysubscriptionoffer) | Subscription offer information | | RenewalType | [AdaptySubscriptionRenewalType](#adaptysubscriptionrenewaltype) | The type of the subscription renewal | | BasePlanId | string (optional) | The identifier of the base plan | ### AdaptySubscriptionPeriod | Name | Type | Description | | :------------ | :--------------- | :------------------------------------------------------------------------------------------------------------------------------- | | Unit | AdaptySubscriptionPeriodUnit | A unit of time that a subscription period is specified in | | NumberOfUnits | long | A number of period units | ### AdaptySubscriptionOffer | Name | Type | Description | |:------------------------------|:------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Identifier | string | Unique identifier of a discount offer for a product | | Type | [AdaptySubscriptionOfferType](#adaptysubscriptionoffertype) | Type of the subscription offer | | Phases | array of [AdaptySubscriptionPhase](#adaptysubscriptionphase) | A list of discount phases available for this offer | | OfferTags | array of strings | Tags defined in Google Play console for current offer | ### AdaptySubscriptionPhase | Name | Type | Description | |:------------------------------|:------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Price | [AdaptyPrice](#adaptyprice) | Price of the discount phase in a local currency | | NumberOfPeriods | int | An integer that indicates the number of periods the product discount is available | | PaymentMode | [AdaptyPaymentMode](#adaptypaymentmode) | The payment mode for this product discount | | SubscriptionPeriod | [AdaptySubscriptionPeriod](#adaptysubscriptionperiod) | A Period object that defines the period for the product discount | | LocalizedSubscriptionPeriod | string (optional) | The formatted subscription period of the discount for the user's localization | | LocalizedNumberOfPeriods | string (optional) | The formatted number of periods of the discount for the user's localization | ### AdaptyPaywall An information about a [paywall.](https://swift.adapty.io/documentation/adapty/adaptypaywall) | Name | Type | Description | |--------------------|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | PlacementId | string | The identifier of the paywall, configured in Adapty Dashboard | | Name | string | Paywall name | | AudienceName | string | Paywall audience name | | ABTestName | string | Paywall A/B test name | | VariationId | string | The identifier of the variation, used to attribute purchases to the paywall | | Revision | int | The current revision (version) of the paywall. Every change within the paywall creates a new revision | | HasViewConfiguration | bool | If true, it is possible to use Adapty Paywall Builder | | Locale | string | An identifier of a paywall locale | | RemoteConfigString | string (optional) | The custom JSON formatted data configured in Adapty Dashboard (String representation) | | RemoteConfig | object (optional) | A custom dictionary configured in Adapty Dashboard for this paywall (same as remoteConfigString) | | VendorProductIds | array of strings | Array of related products ids | ### 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\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. ```csharp showLineNumbers try { builder = builder.SetCustomStringAttribute("string_key", "string_value"); builder = builder.SetCustomDoubleAttribute("double_key", 123.0f); } catch (Exception e) { // handle the exception } ``` To remove existing key, use `.withRemoved(customAttributeForKey:)` method: ```csharp showLineNumbers try { builder = builder.RemoveCustomAttribute("key_to_remove"); } catch (Exception e) { // handle the exception } ``` 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: unity-test.md --- --- title: "Test & release in Unity SDK" description: "Learn how to test and release your Unity app with Adapty SDK." slug: /unity-test displayed_sidebar: sdkunity --- If you've already implemented the Adapty SDK in your Unity 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: unity-troubleshoot-paywall-builder.md --- --- title: "Troubleshoot Paywall Builder in Unity SDK" description: "Troubleshoot Paywall Builder in Unity SDK" --- This guide helps you resolve common issues when using paywalls designed in the Adapty Paywall Builder in the Unity SDK. ## Getting a paywall configuration fails **Issue**: The `CreateView` 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](unity-sdk-migration-guides) if needed. Many issues are resolved in newer SDK versions. --- # File: unity-troubleshoot-purchases.md --- --- title: "Troubleshoot purchases in Unity SDK" description: "Troubleshoot purchases in Unity SDK" --- This guide helps you resolve common issues when implementing purchases manually in the Unity 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-unity) 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](unity-sdk-migration-guides) if needed. Many issues are resolved in newer SDK versions. --- # File: unity-use-fallback-paywalls.md --- --- title: "Work with paywalls offline" description: "Learn how to use fallback paywalls in your Unity app with Adapty SDK." slug: /unity-use-fallback-paywalls displayed_sidebar: sdkunity --- ## Fallback paywalls Fallback paywalls allow your app to work offline by using locally cached paywall data. ## Set fallback paywalls To set fallback paywalls for offline use: ```csharp using Adapty; // Set fallback paywalls var fallbackPaywalls = new ListThe 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 |
The request failed because the specified profile wasn’t found. Double-check the `customer_user_id` or `profile_id` for any typos. ##### Body | Parameter | Type | Description | | ----------- | ------- | ------------------------------------------------------------ | | errors | Object |
Indicates if the attribution is organic or non-organic.
Possible values are:
User ID you use in your app to identify the user if you do. For example, it can be your user UUID, email, or any other ID. Null if you didn't set it. You can find it in the **Customer User ID** field of the profile in the [Adapty Dashboard](https://app.adapty.io/profiles/users).
* Either `customer_user_id` or `profile_id` is required.
| | profile_id | String | :heavy_plus_sign:* |An identifier of a user in Adapty. You can find it in the **Adapty ID** field of the profile in the [Adapty Dashboard](https://app.adapty.io/profiles/users).
* Either `customer_user_id` or `profile_id` is required.
| --- ## Responses ### 201: Created The paywall view is recorded successfully. The response body is blank. ### 400: Bad Request The request failed because the value of the `status` field is invalid. Please check for typos. The possible values are `organic`, `non_organic`, and `unknown`. #### Body | Parameter | Type | Description | | ----------- | ------- | ------------------------------------------------------------ | | errors | Object |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 |
The request failed because the specified profile wasn’t found. Double-check the `customer_user_id` or `profile_id` for any typos. ##### Body | Parameter | Type | Description | | ----------- | ------- | ------------------------------------------------------------ | | errors | Object |
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 |
Locale code for the [paywall localization. It uses language and region subtags separated by a hyphen (**-**).
Examples: `en` for English, `pt-br` for Brazilian Portuguese.
Refer to [Localizations and locale codes](localizations-and-locale-codes) for more details.
| | data | String | :heavy_plus_sign: | Serialized JSON string representing the remote config of your paywall. You can find it in the **Remote Config** tab of a specific paywall in the Adapty Dashboard. | --- # File: web-api-record-paywall-view.md --- --- title: "Record paywall view API request" description: "" displayed_sidebar: APISidebar --- Adapty can help you measure the conversion of your paywalls. However, to do so, it is required for you to log when a paywall gets shown — without that we'd only know about the users who made a purchase and we'd miss those who did not. Use this request to log a paywall view. ## Endpoint and method ```text showLineNumbers POST https://api.adapty.io/api/v2/web-api/paywall/visit/ ```An identifier of a user in your system.
* Either `customer_user_id` or `profile_id` is required.
| | profile_id | String | :heavy_plus_sign:* |An identifier of a user in Adapty.
* Either `customer_user_id` or `profile_id` is required.
| | visited at | ISO 8601 date | :heavy_minus_sign: | The datetime when the user opened the paywall. | | store | String | :heavy_plus_sign: | Store where the product was bought. Possible values: **app_store**, **play_store**, **stripe**, or the **Store ID** of your [custom store](custom-store). | | variation_id | String | :heavy_plus_sign: | The variation ID used to trace purchases to the specific paywall they were made from. | --- ## Responses ### 201: Created The paywall view is recorded successfully. ### 400: Bad Request The request failed because the format of the `visited_at` field is incorrect. Use the **ISO 8601 date** format, e.g. `2025-01-14T14:15:22Z`. #### Body | Parameter | Type | Description | | ----------- | ------- | ------------------------------------------------------------ | | errors | Object |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 |
The request failed because the specified profile wasn’t found. Double-check the `customer_user_id` or `profile_id` for any typos. ##### Body | Parameter | Type | Description | | ----------- | ------- | ------------------------------------------------------------ | | errors | Object |
While custom attribute values in the mobile app code can be set as floats or strings, attributes received via the server-side API or historical import may come in different formats. The boolean and integer values will be converted to floats in this case.
| ### Attribution data If you've chosen to send attribution data and if you have them, the data below will be sent with the event for every source. The same attribution data is sent to all event types. To send the attribution, enable the **Send Attribution** option in the [Integrations -> Webhooks](https://app.adapty.io/integrations/customwebhook) page. ```json title="Json" showLineNumbers { "attributions": { "appsflyer": { "ad_set": "sample_ad_set_123", "status": "non_organic", "channel": "sample_channel", "ad_group": "sample_ad_group_456", "campaign": "sample_ios_campaign", "creative": "sample_creative_789", "created_at": "2000-01-31T00:00:00.000000+0000", "network_user_id": "0000000000000-0000000" } } } ``` | Field name | Field type | Description | | :------------------ | :------------ | :------------------------------------------------- | | **ad_set** | String | Attribution ad set. | | **status** | String | Can be `organic`, `non_organic,` or `unknown`. | | **channel** | String | Marketing channel name. | | **ad_group** | String | Attribution ad group. | | **campaign** | String | Marketing campaign name. | | **creative** | String | Attribution creative keyword. | | **created_at** | ISO 8601 date | Date and time of attribution record creation. | | **network_user_id** | String | ID assigned to the user by the attribution source. | ### Integration IDs The following integration IDs are used now in events: - `adjust_device_id` - `airbridge_device_id` - `amplitude_device_id` - `amplitude_user_id` - `appmetrica_device_id` - `appmetrica_profile_id` - `appsflyer_id` - `branch_id` - `facebook_anonymous_id` - `firebase_app_instance_id` - `mixpanel_user_id` - `pushwoosh_hwid` - `one_signal_player_id` - `one_signal_subscription_id` - `tenjin_analytics_installation_id` ### Play Store purchase token This field includes all the data needed to revalidate a purchase, if necessary. It is sent only if the **Send Play Store purchase token** option is enabled in the [Webhook integration settings](https://app.adapty.io/integrations/customwebhook). | Field | Type | Description | | :------------------ | :------ | :----------------------------------------------------------- | | **product_id** | String | The unique identifier of the product (SKU) purchased in the Play Store. | | **purchase_token** | String | A token generated by Google Play to uniquely identify this purchase transaction. | | **is_subscription** | Boolean | Indicates whether the purchased product is a subscription (`true`) or a one-time purchase (`false`). | ### Event Properties Event properties can vary depending on the event type and even between events of the same type. For instance, an event originating from the App Store won’t include Android-specific properties like `base_plan_id`. The [Access Level Updated](webhook-event-types-and-fields#for-access-level-updated-event) event has distinct properties, so we’ve dedicated a separate section to it. Similarly, we’ve separated [Additional tax and revenue event properties](webhook-event-types-and-fields#additional-tax-and-revenue-event-properties), as they are specific to only certain event types. #### For most event types The event properties for most event types are consistent (except for **Access Level Updated** event, which is described in its own section). Below is a comprehensive table highlighting properties and indicating if they belong to specific events. | Field | Type | Description | | :-------------------------------- | :------------ | :----------------------------------------------------------- | | **ab_test_name** | String | Name of the A/B test where the transaction originated. | | **base_plan_id** | 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. | | **cancellation_reason** | String |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`.
Present in the following event types:
`subscription_cancelled`, `subscription_refunded`, and `trial_cancelled`. | | **cohort_name** | String | Name of the audience to which the profile belongs. | | **consecutive_payments** | Integer | The number of periods, that a user is subscribed to without interruptions. Includes the current period. | | **currency** | String | Local currency (defaults to USD). | | **developer_id** | String | The ID of the placement where the transaction originated. | | **environment** | String | Possible values are `Sandbox` or `Production`. | | **event_datetime** | ISO 8601 date | The date and time of the event. | | **original_purchase_date** | ISO 8601 date | For recurring subscriptions, the original purchase is the first transaction in the chain, its ID called original transaction ID links the chain of renewals; later transactions are extensions of it. The original purchase date is the date and time of this first transaction. | | **original_transaction_id** | String |For recurring subscriptions, this is the original transaction ID that links the chain of renewals. The original transaction is the first in the chain; later transactions are extensions of it.
If no extensions, `original_transaction_id` matches store_transaction_id.
| | **paywall_name** | String | Name of the paywall where the transaction originated. | | **paywall_revision** | Integer | Revision of the paywall where the transaction originated. The default value is 1. | | **price_local** | Float | Product price before Apple/Google cut in local currency. Revenue. | | **price_usd** | Float | Product price before Apple/Google cut in USD. Revenue. | | **profile_country** | String | Determined by Adapty, based on profile IP. | | **profile_event_id** | UUID | Unique event ID that can be used for deduplication. | | **profile_has_access_level** | Boolean | A boolean that indicates whether the profile has an active access level. | | **profile_id** | UUID | Adapty internal user ID. | | **profile_ip_address** | String | Profile IP (can be IPv4 or IPv6, with IPv4 preferred when available). | | **profile_total_revenue_usd** | Float | Total revenue for the profile, refunds included. | | **profiles_sharing_access_level** | JSON |A list of objects, each containing the IDs of users who share the access level (excluding the current profile):
This is used when your app allows [**Sharing paid access between user accounts**](general#6-sharing-purchases-between-user-accounts).
| | **promotional_offer_id** | String | ID of promotional offer as indicated in the Product section of the Adapty Dashboard | | **purchase_date** | ISO 8601 date | The date and time of the product purchase. | | **rate_after_first_year** | Boolean | Boolean indicates that a vendor reduces cuts to 15%. Apple and Google have 30% first-year cut and 15% after it. | | **store** | String | Store where the product was bought. Possible values: **app_store**, **play_store**, **stripe**. | | **store_country** | String | The country sent to us by the app store. | | **store_offer_category** | String | Applied offer category. Possible values are `introductory`, `promotional`, `winback`. | | **store_offer_discount_type** | String | Applied offer type. Possible values are `free_trial`, `pay_as_you_go`, and `pay_up_front`. | | **subscription_expires_at** | ISO 8601 date | The Expiration date of subscription. Usually in the future. | | **transaction_id** | String | Unique identifier for a transaction. | | **trial_duration** | String | Duration of a trial period in days. Sent in a format "{} days", for example, "7 days". Present in the trial connected event types only: `trial_started`, `trial_converted`, `trial_cancelled`. | | **variation_id** | UUID | Unique ID of the paywall where the purchase was made. | | **vendor_product_id** | String | Product ID in the Apple App Store, Google Play Store, or Stripe. | #### Additional tax and revenue event properties The event properties related to taxes and revenue below are additional fields that apply only to certain event types. This means that the listed event types include the [Event properties for most event types](webhook-event-types-and-fields#for-most-event-types), along with the extra fields listed below. Event types that have the tax and revenue event properties: - `subscription_renewed` - `subscription_initial_purchase` - `subscription_refunded` - `non_subscription_purchase` | Field | Type | Description | | :-------------------- | :---- | :----------------------------------------------------------- | | **net_revenue_local** | Float | Net revenue (income after Apple/Google cut and taxes) in local currency. | | **net_revenue_usd** | Float | Net revenue (income after Apple/Google cut and taxes) in USD. | | **proceeds_local** | Float | Product price after Apple/Google cut in local currency. | | **proceeds_usd** | Float | Product price after Apple/Google cut. | | **tax_amount_local** | Float | Tax amount deducted in local currency. | | **tax_amount_usd** | Float | Tax amount deducted in USD. | #### For Access Level Updated event The **Access Level Updated** event is a specific webhook event generated only when the Webhook integration is active, and this event type is enabled. If enabled, it is sent to the configured Webhook and appears in the **Event Feed**. If not enabled, the event will not be created. | Property | Type | Description | | ---------------------------------- | ------------- | ------------------------------------------------------------ | | **ab_test_name** | String | Name of the A/B test where the transaction originated. | | **access_level_id** | String | The ID of the access level. | | **activated_at** | ISO 8601 date | Date and time when the access was latest activated. | | **active_introductory_offer_type** | String | Type of introductory offer applied. Possible values are `free_trial`, `pay_as_you_go`, and `pay_up_front`. | | **active_promotional_offer_id** | String | ID of promotional offer as indicated in the Product section of the Adapty Dashboard | | **active_promotional_offer_type** | String | Type of promotional offer applied. Possible values are `free_trial`, `pay_as_you_go`, and `pay_up_front`. | | **base_plan_id** | 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. | | **billing_issue_detected_at** | ISO 8601 date | Date and time of billing issue. | | **cancellation_reason** | String | 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`. | | **cohort_name** | String | Name of the audience to which the profile belongs. | | **currency** | String | Local currency (defaults to USD). | | **developer_id** | String | The ID of the placement where the transaction originated. | | **environment** | String | Possible values are `Sandbox` or `Production`. | | **event_datetime** | ISO 8601 date | The date and time of the event. | | **expires_at** | ISO 8601 date | Date and time when the access will expire. | | **is_active** | Boolean | Boolean indicating whether the access level is active. | | **is_in_grace_period** | Boolean | Boolean indicating whether the profile is in the grace period. | | **is_lifetime** | Boolean | Boolean indicating whether the access level is lifetime. | | **is_refund** | Boolean | Boolean indicating whether the transaction is a refund. | | **original_purchase_date** | ISO 8601 date | For recurring subscriptions, the original purchase is the first transaction in the chain, its ID called original transaction ID links the chain of renewals; later transactions are extensions of it. The original purchase date is the date and time of this first transaction. | | **original_transaction_id** | String |For recurring subscriptions, this is the original transaction ID that links the chain of renewals. The original transaction is the first in the chain; later transactions are extensions of it.
If no extensions, `original_transaction_id` matches store_transaction_id.
The transaction identifier of the original purchase. | | **paywall_name** | String | Name of the paywall where the transaction originated. | | **paywall_revision** | Integer | Revision of the paywall where the transaction originated. The default value is 1. | | **profile_country** | String | Determined by Adapty, based on profile IP. | | **profile_event_id** | UUID | Unique event ID that can be used for deduplication. | | **profile_has_access_level** | Boolean | Boolean indicating whether the profile has an active access level. | | **profile_id** | UUID | Adapty internal user profile ID. | | **profile_ip_address** | String | Profile IP address of the user. | | **profile_total_revenue_usd** | Float | Total revenue for the profile, refunds included. | | **profiles_sharing_access_level** | JSON |A list of objects, each containing the IDs of users who share the access level (excluding the current profile):
This is used when your app allows [**Sharing paid access between user accounts**](general#6-sharing-purchases-between-user-accounts).
| | **purchase_date** | ISO 8601 date | The date and time of product purchase. | | **renewed_at** | ISO 8601 date | Date and time when the access will be renewed. | | **store** | String | Store where the product was bought. Possible values: **app_store**, **play_store**, **stripe**. | | **store_country** | String | Country sent to Adapty by the app store. | | **subscription_expires_at** | ISO 8601 date | Expiration date of the subscription. | | **transaction_id** | String | Unique identifier for a transaction. | | **trial_duration** | String | Duration of a trial period in days (e.g., "7 days"). | | **variation_id** | UUID | An identifier of a variation, used to attribute purchases to this paywall. | | **vendor_product_id** | String | Product ID in the store (Apple/Google/Stripe). | | **will_renew** | Boolean | Indicates whether the paid access level will be renewed. | :::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 specific fields rather than the entire structure. ::: --- # File: webhook.md --- --- title: "Webhook integration" description: "Integrate webhooks in Adapty to automate subscription event tracking." --- A webhook is an efficient way to receive real-time notifications about [events](webhook-event-types-and-fields#webhook-event-types), especially for tracking subscription and purchase changes. This allows you to monitor subscriber status and react accordingly. Unlike API requests that require constant polling, a webhook is configured once and automatically sends data via HTTP when an event occurs. With webhooks integrated, you can: - Keep track of subscriptions and purchases in your backend system. - Automate processes and workflows based on subscription lifecycles. - Engage with subscribers by reminding them of app benefits, addressing unsubscribe decisions, and handling billing issues. - Conduct a detailed analysis of the user behavior. **Integration characteristics** | Integration characteristic | Description | | :------------------------- | :---------------------------------------------------------- | | Schedule | Real-time updates | | Data direction | One-way data transmission: from Adapty to your server | | Adapty integration flow | Events are sent by the Adapty server once they are received | ## Events sent to webhook You can see all event types that can be sent to a webhook in the [Webhook event types and fields](webhook-event-types-and-fields) page. You can send all of them to your webhook or choose only some of them. Consult our [Event flows](event-flows) page to decide which events are required or not. You can disable the event types you do not need when you [set up your Webhook integration](set-up-webhook-integration#configure-webhook-integration-in-the-adapty-dashboard). There, you can also replace the Adapty default event IDs with your own if required. **What's next:** - [Webhook event types and fields](webhook-event-types-and-fields): Explore detailed descriptions of each event and their data fields. - [Event flows](event-flows): Learn about the sequence of events and their dependencies. - [Set up webhook integration](set-up-webhook-integration): Step-by-step guidance on configuring your webhook in the Adapty Dashboard. - [Test webhook integration](test-webhook): Ensure your webhook is set up correctly with our testing tools. --- # File: what-is-adapty.md --- --- title: "What is Adapty" description: "Learn what Adapty is and how it helps manage subscriptions." slug: / --- Adapty is a powerful and adaptable in-app purchase platform that helps you grow your subscriber base. Whether you're just starting or already have millions of users, Adapty makes it easy to set up the best subscription prices, test different approaches, and see what works best for your app's success. ### Marketing automation and testing :::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 using AI with our docs. ::: - Subscriptions/in-app purchases SDK for [iOS](sdk-installation-ios), [Android](sdk-installation-android), [Flutter](sdk-installation-flutter), [React Native,](sdk-installation-reactnative) and [Unity](sdk-installation-unity). Adapty performs server-side receipt validation for you and syncs your customers across all platforms, including the [web](getting-started-with-server-side-api). It also works in [Observer mode](observer-vs-full-mode), so you can use SDK without changing your existing purchase infrastructure. - [A/B testing](ab-tests) for subscription plans. Test different prices, durations, and trial periods for your subscriptions as well as different visual elements. - [Analytics](analytics-charts) for the app economy. Detailed metrics related to your app monetization. - Adapty can send [subscription events](events) to third party analytics: [Amplitude](amplitude), [AppsFlyer](appsflyer), [Adjust](adjust), [Branch](branch), [Mixpanel](mixpanel), [Facebook Ads](facebook-ads), [AppMetrica](appmetrica), and custom [Webhook](webhook). ### Adapty works for developers, marketers, and executives Marketers can directly engage users with promotional offers to return them to the service or upsell new products. With Adapty, there is no need for programmers and analysts to manually extract segments. For product managers and executives, Adapty has a dashboard with viable subscription metrics with daily/weekly/monthly reports to Slack and Emails. --- # End of Documentation _Generated on: 2025-09-02T07:17:20.100Z_