# CAPACITOR - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Generated on: 2026-02-23T15:34:19.820Z Total files: 37 --- # File: capacitor-sdk-overview --- --- title: "Capacitor SDK overview" description: "Learn about Adapty Capacitor SDK and its key features." --- [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-Capacitor.svg?style=flat&logo=capacitor)](https://github.com/adaptyteam/AdaptySDK-Capacitor/releases) Welcome! We're here to make in-app purchases a breeze 🚀 We've built the [Adapty Capacitor SDK](https://github.com/adaptyteam/AdaptySDK-Capacitor/) to take the headache out of in-app purchases so you can focus on what you do best – building amazing apps. Here's what we handle for you: - Handle purchases, receipt validation, and subscription management out of the box - Create and test paywalls without app updates - Get detailed purchase analytics with zero setup - cohorts, LTV, churn, and funnel analysis included - Keep the user subscription status always up to date across app sessions and devices - Integrate your app with marketing attribution and analytics services using just one line of code :::note Before diving into the code, you'll need to integrate Adapty with App Store Connect and Google Play Console, then set up products in the dashboard. Check out our [quickstart guide](quickstart.md) to get everything configured first. ::: ## Get started :::tip Our docs are optimized for use with LLMs. Check out [this article](adapty-cursor-capacitor.md) to learn how to get the best results when integrating the Adapty SDK using AI with our docs. ::: Here's what we'll cover in the integration guide: 1. [Install & configure SDK](sdk-installation-capacitor.md): Add the SDK as a [dependency](https://www.npmjs.com/package/@adapty/capacitor) to your project and activate it in the code. 2. [Enable purchases through paywalls](capacitor-quickstart-paywalls.md): Set up the purchase flow so users can buy products. 3. [Check the subscription status](capacitor-check-subscription-status.md): Automatically check the user's subscription state and control their access to paid content. 4. [Identify users (optional)](capacitor-quickstart-identify.md): Associate users with their Adapty profiles to ensure their data is stored consistently across devices. ### See it in action Want to see how it all comes together? We've got you covered: **Sample apps**: Check out our complete examples that demonstrate the full setup: - [React](https://github.com/adaptyteam/AdaptySDK-Capacitor/tree/master/examples/basic-react-example) - [Vue.js](https://github.com/adaptyteam/AdaptySDK-Capacitor/tree/master/examples/basic-vue-example) - [Angular](https://github.com/adaptyteam/AdaptySDK-Capacitor/tree/master/examples/basic-angular-example) - [Advanced development tools](https://github.com/adaptyteam/AdaptySDK-Capacitor/tree/master/examples/adapty-devtools) ## Main concepts Before diving into the code, let's get familiar with the key concepts that make Adapty work. The beauty of Adapty's approach is that only placements are hardcoded in your app. Everything else – products, paywall designs, pricing, and offers – can be managed flexibly from the Adapty dashboard without app updates: 1. **Product** - Anything available for purchase in your app – subscription, consumable product, or lifetime access. 2. **Paywall** - The only way to retrieve products from Adapty and use it to its full power. We've designed it this way to make it easier to track how different product combinations affect your monetization metrics. A paywall in Adapty serves as both a specific set of your products and the visual configuration that accompanies them. 3. **Placement** - A strategic point in your user journey where you want to show a paywall. Think of placements as the "where" and "when" of your monetization strategy. Common placements include: - `main` - Your primary paywall location - `onboarding` - Shown during the user onboarding flow - `settings` - Accessible from your app's settings Start with the basics like `main` or `onboarding` for your first integration, then think about where else in your app users might be ready to purchase. 4. **Profile** - When users purchase a product, their profile is assigned an **access level** which you use to define access to paid features. --- # File: sdk-installation-capacitor --- --- title: "Capacitor - Adapty SDK installation & configuration" description: "Step-by-step guide on installing Adapty SDK on Capacitor for subscription-based apps." --- Adapty SDK includes two key modules for seamless integration into your Capacitor app: - **Core Adapty**: This module is required for Adapty to function properly in your app. - **AdaptyUI**: This module is needed if you use the [Adapty Paywall Builder](adapty-paywall-builder), a user-friendly, no-code tool for easily creating cross-platform paywalls. AdaptyUI is automatically activated along with the core module. :::tip Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](https://github.com/adaptyteam/AdaptySDK-Capacitor/tree/master/example-app), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality. ::: ## Requirements The [Adapty Capacitor SDK](https://github.com/adaptyteam/AdaptySDK-Capacitor/) has the following version requirements: | Adapty SDK Version | Capacitor Version | iOS Version | |--------------------|-------------------|-------------| | 3.16.0+ | 8 | 15.0+ | | 3.15 | 7 | 14.0+ | Capacitor versions 6 and below are not supported. :::info Adapty is compatible with Google Play Billing Library up to 8.x. By default, Adapty works with Google Play Billing Library v.7.0.0 but, if you want to force a later version, you can manually [add the dependency](https://developer.android.com/google/play/billing/integrate#dependency). ::: ## Install Adapty SDK [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-Capacitor.svg?style=flat&logo=capacitor)](https://github.com/adaptyteam/AdaptySDK-Capacitor/releases) Install Adapty SDK: ```sh npm install @adapty/capacitor npx cap sync ``` ## Activate Adapty module of Adapty SDK :::note The Adapty SDK only needs to be activated once in your app. ::: To get your **Public SDK Key**: 1. Go to Adapty Dashboard and navigate to [**App settings → General**](https://app.adapty.io/settings/general). 2. From the **Api keys** section, copy the **Public SDK Key** (NOT the Secret Key). 3. Replace `"YOUR_PUBLIC_SDK_KEY"` in the code. :::important - Make sure you use the **Public SDK key** for Adapty initialization, the **Secret key** should be used for [server-side API](getting-started-with-server-side-api) only. - **SDK keys** are unique for every app, so if you have multiple apps make sure you choose the right one. ::: Copy the following code to any app file to activate Adapty: ```typescript showLineNumbers try { await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { // verbose logging is recommended for the development purposes and for the first production release logLevel: 'verbose', // in the development environment, use this variable to avoid multiple activation errors. Set it to your development environment variable __ignoreActivationOnFastRefresh: true, } }); console.log('Adapty activated successfully!'); } catch (error) { console.error('Failed to activate Adapty SDK:', error); } ``` :::tip To avoid activation errors in the development environment, use the [tips](#development-environment-tips). ::: Now set up paywalls in your app: - If you use [Adapty Paywall Builder](adapty-paywall-builder), follow the [Paywall Builder quickstart](capacitor-quickstart-paywalls). - If you build your own paywall UI, see the [quickstart for custom paywalls](capacitor-quickstart-manual). ## Activate AdaptyUI module of Adapty SDK If you plan to use [Paywall Builder](adapty-paywall-builder.md), you need the AdaptyUI module. It is done automatically when you activate the core module; you don't need to do anything else. ## Optional setup ### Logging #### Set up the logging system Adapty logs errors and other important information to help you understand what is going on. There are the following levels available: | Level | Description | | ---------- | ------------------------------------------------------------ | | `error` | Only errors will be logged | | `warn` | Errors and messages from the SDK that do not cause critical errors, but are worth paying attention to will be logged | | `info` | Errors, warnings, and various information messages will be logged | | `verbose` | Any additional information that may be useful during debugging, such as function calls, API queries, etc. will be logged | You can set the log level in your app before or during Adapty configuration: ```typescript showLineNumbers // Set log level before activation adapty.setLogLevel({ logLevel: 'verbose' }); // Or set it during configuration await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { logLevel: 'verbose', } }); ``` ### Data policies Adapty doesn't store personal data of your users unless you explicitly send it, but you can implement additional data security policies to comply with the store or country guidelines. #### Disable IP address collection and sharing When activating the Adapty module, set `ipAddressCollectionDisabled` to `true` to disable user IP address collection and sharing. The default value is `false`. Use this parameter to enhance user privacy, comply with regional data protection regulations (like GDPR or CCPA), or reduce unnecessary data collection when IP-based features aren't required for your app. ```typescript showLineNumbers await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { ipAddressCollectionDisabled: true, } }); ``` #### Disable advertising ID collection and sharing When activating the Adapty module, set `ios.idfaCollectionDisabled` (iOS) or `android.adIdCollectionDisabled` (Android) to `true` to disable the collection of advertising identifiers. The default value is `false`. Use this parameter to comply with App Store/Play Store policies, avoid triggering the App Tracking Transparency prompt, or if your app does not require advertising attribution or analytics based on advertising IDs. ```typescript showLineNumbers await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { ios: { idfaCollectionDisabled: true, }, android: { adIdCollectionDisabled: true, }, } }); ``` #### Set up media cache configuration for AdaptyUI By default, AdaptyUI caches media (such as images and videos) to improve performance and reduce network usage. You can customize the cache settings by providing a custom configuration. Use `mediaCache` to override the default cache settings: ```typescript showLineNumbers await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { mediaCache: { memoryStorageTotalCostLimit: 200 * 1024 * 1024, // Optional: memory cache size in bytes memoryStorageCountLimit: 2147483647, // Optional: max number of items in memory diskStorageSizeLimit: 200 * 1024 * 1024, // Optional: disk cache size in bytes }, } }); ``` Parameters: | Parameter | Required | Description | |-----------|----------|-------------| | memoryStorageTotalCostLimit | optional | Total cache size in memory in bytes. Defaults to platform-specific value. | | memoryStorageCountLimit | optional | The item count limit of the memory storage. Defaults to platform-specific value. | | diskStorageSizeLimit | optional | The file size limit on disk in bytes. Defaults to platform-specific value. | ### Enable local access levels (Android) By default, [local access levels](local-access-levels.md) are enabled on iOS and disabled on Android. To enable them on Android as well, set `localAccessLevelAllowed` to `true`: ```typescript showLineNumbers await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { android: { localAccessLevelAllowed: true, }, } }); ``` ### Clear data on backup restore When `clearDataOnBackup` is set to `true`, the SDK detects when the app is restored from an iCloud backup and deletes all locally stored SDK data, including cached profile information, product details, and paywalls. The SDK then initializes with a clean state. Default value is `false`. :::note Only local SDK cache is deleted. Transaction history with Apple and user data on Adapty servers remain unchanged. ::: ```swift showLineNumbers await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { ios: { clearDataOnBackup: true, }, } }); ``` ## Development environment tips #### Troubleshoot SDK activation errors on Capacitor's live-reload When developing with the Adapty SDK in Capacitor, you may encounter the error: `Adapty can only be activated once. Ensure that the SDK activation call is not made more than once.` This occurs because Capacitor's live-reload feature triggers multiple activation calls during development. To prevent this, use the `__ignoreActivationOnFastRefresh` option set to the Capacitor's development mode flag – it will differ depending on the bundle you are using. ```typescript showLineNumbers try { await adapty.activate({ apiKey: 'YOUR_PUBLIC_SDK_KEY', params: { // Set your development environment variable __ignoreActivationOnFastRefresh: true, } }); } catch (error) { console.error('Failed to activate Adapty SDK:', error); // Handle the error appropriately for your app } ``` ## Troubleshooting #### Minimum iOS version error If you get a minimum iOS version error, update your Podfile: ```diff -platform :ios, min_ios_version_supported +platform :ios, '14.0' # For core features only # OR +platform :ios, '15.0' # If using paywalls created in the paywall builder ``` #### Android backup rules (Auto Backup configuration) Some SDKs (including Adapty) ship their own Android Auto Backup configuration. If you use multiple SDKs that define backup rules, the Android manifest merger can fail with an error mentioning `android:fullBackupContent`, `android:dataExtractionRules`, or `android:allowBackup`. Typical error symptoms: `Manifest merger failed: Attribute application@dataExtractionRules value=(@xml/your_data_extraction_rules) is also present at [com.other.sdk:library:1.0.0] value=(@xml/other_sdk_data_extraction_rules)` :::note These changes should be made in your Android platform directory (typically located in your project's `android/` folder). ::: To resolve this, you need to: - Tell the manifest merger to use your app's values for backup-related attributes. - Create backup rule files that merge Adapty's rules with rules from other SDKs. #### 1. Add the `tools` namespace to your manifest In your `AndroidManifest.xml` file, ensure the root `` tag includes tools: ```xml ... ``` #### 2. Override backup attributes in `` In the same `AndroidManifest.xml` file, update the `` tag so that your app provides the final values and tells the manifest merger to replace library values: ```xml ... ``` If any SDK also sets `android:allowBackup`, include it in `tools:replace` as well: ```xml tools:replace="android:allowBackup,android:fullBackupContent,android:dataExtractionRules" ``` #### 3. Create merged backup rules files Create XML files in your Android project's `res/xml/` directory that combine Adapty's rules with rules from other SDKs. Android uses different backup rule formats depending on the OS version, so creating both files ensures compatibility across all Android versions your app supports. :::note The examples below show AppsFlyer as a sample third-party SDK. Replace or add rules for any other SDKs you're using in your app. ::: **For Android 12 and higher** (uses the new data extraction rules format): ```xml title="sample_data_extraction_rules.xml" ``` **For Android 11 and lower** (uses the legacy full backup content format): ```xml title="sample_backup_rules.xml" :::tip After changing native Android files, run `npx cap sync android` so Capacitor picks up the updated resources if you regenerate the platform. ::: #### Purchases fail after returning from another app in Android If the Activity that starts the purchase flow uses a non-default `launchMode`, Android may recreate or reuse it incorrectly when the user returns from Google Play, a banking app, or a browser. This can cause the purchase result to be lost or treated as canceled. To ensure purchases work correctly, use only `standard` or `singleTop` launch modes for the Activity that starts the purchase flow, and avoid any other modes. In your `AndroidManifest.xml`, ensure the Activity that starts the purchase flow is set to `standard` or `singleTop`: ```xml ``` --- # File: capacitor-quickstart-paywalls --- --- title: "Enable purchases by using paywalls in Capacitor SDK" description: "Learn how to present paywalls in your Capacitor app with Adapty SDK." --- To enable in-app purchases, you need to understand three key concepts: - **Products** – anything users can buy (subscriptions, consumables, lifetime access) - **Paywalls** are configurations that define which products to offer. In Adapty, paywalls are the only way to retrieve products, but this design lets you modify offerings, pricing, and product combinations without touching your app code. - **Placements** – where and when you show paywalls in your app (like `main`, `onboarding`, `settings`). You set up paywalls for placements in the dashboard, then request them by placement ID in your code. This makes it easy to run A/B tests and show different paywalls to different users. Adapty offers you three ways to enable purchases in your app. Select one of them depending on your app requirements: | Implementation | Complexity | When to use | |----------------------------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Adapty Paywall Builder** | ✅ Easy | You [create a complete, purchase-ready paywall in the no-code builder](quickstart-paywalls). Adapty automatically renders it and handles all the complex purchase flow, receipt validation, and subscription management behind the scenes. | | `makePurchase` | 🟡 Medium | You implement your paywall UI in your app code, but still get the paywall object from Adapty to maintain flexibility in product offerings. See the [guide](capacitor-quickstart-manual). | | Observer mode | 🔴 Hard | You implement the purchase flow yourself completely. See the [guide](implement-observer-mode-capacitor). | :::important **The steps below show how to implement a paywall created in the Adapty paywall builder.** If you don't want to use the paywall builder, see the [guide for handling purchases in manually created paywalls](capacitor-making-purchases.md). ::: To display a paywall created in the Adapty paywall builder, in your app code, you only need to: 1. **Get the paywall**: Get the paywall from Adapty. 2. **Display the paywall and Adapty will handle purchases for you**: Show the paywall container you've got in your app. 3. **Handle button actions**: Associate user interactions with the paywall with your app's response to them. For example, open links or close the paywall when users click buttons. ## Before you start Before you start, complete these steps: 1. Connect your app to the [App Store](initial_ios) and/or [Google Play](initial-android) in the Adapty Dashboard. 2. [Create your products](create-product) in Adapty. 3. [Create a paywall and add products to it](create-paywall). 4. [Create a placement and add your paywall to it](create-placement). 5. [Install and activate the Adapty SDK](sdk-installation-capacitor) in your app code. :::tip The fastest way to complete these steps is to follow the [quickstart guide](quickstart). ::: ## 1. Get the paywall Your paywalls are associated with placements configured in the dashboard. Placements allow you to run different paywalls for different audiences or to run [A/B tests](ab-tests.md). To get a paywall created in the Adapty paywall builder, you need to: 1. Get the `paywall` object by the [placement](placements.md) ID using the `getPaywall` method and check whether it is a paywall created in the builder using the `hasViewConfiguration` property. 2. Create the paywall view using the `createPaywallView` method. The view contains the UI elements and styling needed to display the paywall. :::important To get the view configuration, you must switch on the **Show on device** toggle in the Paywall Builder. Otherwise, you will get an empty view configuration, and the paywall won't be displayed. ::: ```typescript showLineNumbers title="Capacitor" try { const paywall = await adapty.getPaywall({ placementId: 'YOUR_PLACEMENT_ID', }); // the requested paywall } catch (error) { // handle the error } if (paywall.hasViewConfiguration) { try { const view = await createPaywallView(paywall); } catch (error) { // handle the error } } else { // use your custom logic } ``` ## 2. Display the paywall Now, when you have the paywall configuration, it's enough to add a few lines to display your paywall. To display the paywall, use the `view.present()` method on the `view` created by the `createPaywallView` method. Each `view` can only be used once. If you need to display the paywall again, call `createPaywallView` one more to create a new `view` instance. ```typescript showLineNumbers title="Capacitor" try { await view.present(); } catch (error) { // handle the error } ``` :::tip For more details on how to display a paywall, see our [guide](capacitor-present-paywalls.md). ::: ## 3. Handle button actions When users click buttons in the paywall, the Capacitor SDK automatically handles purchases, restoration, and closing the paywall. However, other buttons have custom or pre-defined IDs and require handling actions in your code. Or, you may want to override their default behavior. For example, you may want to keep the paywall open after your app users open a web link. Let's see how you can handle it in your implementation. :::tip Read our guides on how to handle button [actions](capacitor-handle-paywall-actions.md) and [events](capacitor-handling-events.md). ::: ```typescript showLineNumbers title="Capacitor" const unsubscribe = view.setEventHandlers({ onUrlPress(url) { window.open(url, '_blank'); return false; }, }); ``` ## Next steps Your paywall is ready to be displayed in the app. Test your purchases in the [App Store sandbox](test-purchases-in-sandbox) or in [Google Play Store](testing-on-android) to make sure you can complete a test purchase from the paywall. Now, you need to [check the users' access level](capacitor-check-subscription-status.md) to ensure you display a paywall or give access to paid features to right users. ## Full example Here is how all those steps can be integrated in your app together. ```typescript showLineNumbers title="Capacitor" export default function PaywallScreen() { const showPaywall = async () => { try { const paywall = await adapty.getPaywall({ placementId: 'YOUR_PLACEMENT_ID', }); if (!paywall.hasViewConfiguration) { // use your custom logic return; } const view = await createPaywallView(paywall); view.setEventHandlers({ onUrlPress(url) { window.open(url, '_blank'); return false; }, }); await view.present(); } catch (error) { // handle any error that may occur during the process console.warn('Error showing paywall:', error); } }; return (
); } ``` --- # File: capacitor-check-subscription-status --- --- title: "Check subscription status in Capacitor SDK" description: "Learn how to check subscription status in your Capacitor app with Adapty." --- To decide whether users can access paid content or see a paywall, you need to check their [access level](access-level.md) in the profile. This article shows you how to access the profile state to decide what users need to see - whether to show them a paywall or grant access to paid features. ## Get subscription status When you decide whether to show a paywall or paid content to a user, you check their [access level](access-level.md) in their profile. You have two options: - Call `getProfile` if you need the latest profile data immediately (like on app launch) or want to force an update. - Set up **automatic profile updates** to keep a local copy that's automatically refreshed whenever the subscription status changes. ### Get profile The easiest way to get the subscription status is to use the `getProfile` method to access the profile: ```typescript showLineNumbers try { const profile = await adapty.getProfile(); } catch (error) { // handle the error } ``` ### Listen to subscription updates To automatically receive profile updates in your app: 1. Use `adapty.addListener('onLatestProfileLoad')` to listen for profile changes - Adapty will automatically call this method whenever the user's subscription status changes. 2. Store the updated profile data when this method is called, so you can use it throughout your app without making additional network requests. ```typescript showLineNumbers class SubscriptionManager { private currentProfile: any = null; constructor() { // Listen for profile updates adapty.addListener('onLatestProfileLoad', (data) => { this.currentProfile = data.profile; // Update UI, unlock content, etc. }); } // Use stored profile instead of calling getProfile() hasAccess(): boolean { return this.currentProfile?.accessLevels?.['YOUR_ACCESS_LEVEL']?.isActive ?? false; } } ``` :::note Adapty automatically calls the `onLatestProfileLoad` event listener when your app starts, providing cached subscription data even if the device is offline. ::: ## Connect profile with paywall logic When you need to make immediate decisions about showing paywalls or granting access to paid features, you can check the user's profile directly. This approach is useful for scenarios like app launch, when entering premium sections, or before displaying specific content. ```typescript showLineNumbers const checkAccessLevel = async () => { try { const profile = await adapty.getProfile(); return profile?.accessLevels?.['YOUR_ACCESS_LEVEL']?.isActive === true; } catch (error) { console.warn('Error checking access level:', error); return false; // Show paywall if access check fails } }; const getAccessLevel = () => { return profile?.accessLevels?.['YOUR_ACCESS_LEVEL']; }; const initializePaywall = async () => { try { await loadPaywall(); const hasAccess = await checkAccessLevel(); if (!hasAccess) { // Show paywall if no access } } catch (error) { console.warn('Error initializing paywall:', error); } }; ``` ## Next steps Now, when you know how to track the subscription status, learn how to [work with user profiles](capacitor-quickstart-identify.md) to ensure they can access what they have paid for. --- # File: capacitor-quickstart-identify --- --- title: "Identify users in Capacitor SDK" description: "Quickstart guide to setting up Adapty for in-app subscription management in Capacitor." --- How you manage users' purchases depends on your app's authentication model: - If your app doesn't use backend authentication and doesn't store user data, see the [section about anonymous users](#anonymous-users). - If your app has (or will have) backend authentication, see the [section about identified users](#identified-users). :::tip **Key concepts**: - **Profiles** are the entities required for the SDK to work. Adapty creates them automatically. They can be anonymous (without customer user ID) or identified (with customer user ID). - **Customer user IDs** are optional identifiers **you create** for Adapty to link your users to their Adapty profiles. ::: Here is what is different for anonymous and identified users: | | Anonymous users | Identified users | |-------------------------|------------------------------------------------------|-------------------------------------------------------------------------| | **Purchase management** | Store-level purchase restoration | Maintain purchase history across devices through their customer user ID | | **Profile management** | New profiles on each reinstall | The same profile across sessions and devices | | **Data persistence** | Anonymous users' data is tied to device/installation | Identified users' data persists across devices and sessions | ## Anonymous users If you don't have backend authentication, **you don't need to handle authentication in the app code**: 1. When the SDK is activated on the app's first launch, Adapty **creates a new profile for the user**. 2. When the user purchases anything in the app, this purchase is **associated with their Adapty profile and their store account**. 3. When the user **re-installs** the app or installs it from a **new device**, Adapty **creates a new empty profile on activation**. 4. If the user has previously made purchases in your app, by default, their purchases are automatically synced from the App Store on the SDK activation. :::note Backup restores behave differently from reinstalls. By default, when a user restores from a backup, the SDK preserves cached data and does not create a new profile. You can configure this behavior using the `clearDataOnBackup` setting. [Learn more](sdk-installation-capacitor#clear-data-on-backup-restore). ::: ## Identified users - If a profile doesn't have a customer user ID yet (meaning, **the user isn't signed in**), when you send a customer user ID, it gets associated with that profile. - If it is a **re-installation, signing in, or installation from a new device**, and you have sent their customer user ID before, a new profile is not created. Instead, we switch to the existing profile associated with the customer user ID. You have two options to identify users in the app: - [**During login/signup:**](#during-loginsignup) If users sign in after your app starts, call `identify()` with a customer user ID when they authenticate. - [**During the SDK activation:**](#during-the-sdk-activation) If you already have a customer user ID stored when the app launches, send it when calling `activate()`. :::important By default, when Adapty receives a purchase from a Customer User ID that is currently associated with another Customer User ID, the access level is shared, so both profiles have paid access. You can configure this setting to transfer paid access from one profile to another or disable sharing completely. See the [article](general#6-sharing-purchases-between-user-accounts) for more details. ::: ### During login/signup If you're identifying users after the app launch (for example, after they log into your app or sign up), use the `identify` method to set their customer user ID. - If you **haven't used this customer user ID before**, Adapty will automatically link it to the current profile. - If you **have used this customer user ID to identify the user before**, Adapty will switch to working with the profile associated with this customer user ID. :::tip When creating a customer user ID, save it with your user data so you can send the same ID when they log in from new devices or reinstall your app. ::: ```typescript showLineNumbers try { await adapty.identify({ customerUserId: "YOUR_USER_ID" }); // successfully identified } catch (error) { // handle the error } ``` ### During the SDK activation If you already know a customer user ID when you activate the SDK, you can send it in the `activate` method instead of calling `identify` separately. If you know a customer user ID but set it only after the activation, that will mean that, upon activation, Adapty will create a new empty profile and switch to the existing one only after you call `identify`. You can pass either an existing customer user ID (the one you have used before) or a new one. If you pass a new one, a new profile created on the activation will be automatically linked to the customer user ID. :::tip To exclude created empty profiles from the dashboard [analytics](analytics-charts.md), go to **App settings** and set up [**Installs definition for analytics**](general#4-installs-definition-for-analytics). ::: ```typescript showLineNumbers await adapty.activate({ apiKey: "YOUR_PUBLIC_SDK_KEY", params: { customerUserId: "YOUR_USER_ID" } }); ``` ### Log users out If you have a button for logging users out, use the `logout` method. This creates a new anonymous profile ID for the user. ```typescript showLineNumbers try { await adapty.logout(); // successful logout } catch (error) { // handle the error } ``` :::info To log users back into the app, use the `identify` method. ::: ### Allow purchases without login If your users can make purchases both before and after they log into your app, you don't need to do additional setup: Here's how it works: 1. When a logged-out user makes a purchase, Adapty ties it to their anonymous profile ID. 2. When the user logs into their account, Adapty switches to working with their identified profile. - If it is an existing customer user ID (the customer user ID is already linked to a profile), Adapty syncs its transactions automatically. - If it is a new customer user ID (e.g., the purchase has been made before registration), Adapty assigns the customer user ID to the current profile, so all the purchase history is maintained. --- # File: adapty-cursor-capacitor --- --- title: "Integrate Adapty into your Capacitor app with AI assistance" description: "A step-by-step guide to integrating Adapty into your Capacitor app using Cursor, Context7, ChatGPT, Claude, or other AI tools." --- This guide helps you integrate Adapty into your Capacitor app with the help of an LLM. You'll start by preparing your Adapty dashboard, then work through each implementation stage by sending focused doc links to your LLM. At the end, you'll find best practices for setting up your AI tools with Adapty documentation. :::tip Copy this entire page as Markdown and paste it into your LLM to get started — click **Copy for LLM** at the top of the page or open [the .md version](https://adapty.io/docs/adapty-cursor-capacitor.md). The LLM will use the guide links and checkpoints to walk you through each stage. ::: ## Before you start: dashboard checklist Adapty requires dashboard configuration before you write any SDK code. Your LLM cannot look up dashboard values for you — you'll need to provide them. ### Required before coding 1. **Connect your app stores**: In the Adapty Dashboard, go to **App settings → General**. Connect both App Store and Google Play if your Capacitor app targets both platforms. This is required for purchases to work. [Connect app stores](integrate-payments.md) 2. **Copy your Public SDK key**: In the Adapty Dashboard, go to **App settings → General**, then find the **API keys** section. In code, this is the string you pass to `adapty.activate()`. 3. **Create at least one product**: In the Adapty Dashboard, go to the **Products** page. You don't reference products directly in code — Adapty delivers them through paywalls. [Add products](quickstart-products.md) 4. **Create a paywall and a placement**: In the Adapty Dashboard, create a paywall on the **Paywalls** page, then assign it to a placement on the **Placements** page. In code, the placement ID is the string you pass to `adapty.getPaywall()`. [Create paywall](quickstart-paywalls.md) 5. **Set up access levels**: In the Adapty Dashboard, configure per product on the **Products** page. In code, the string checked in `profile.accessLevels['premium']?.isActive`. The default `premium` access level works for most apps. If paying users get access to different features depending on the product (for example, a `basic` plan vs. a `pro` plan), [create additional access levels](assigning-access-level-to-a-product.md) before you start coding. :::tip Once you have all five, you're ready to write code. Tell your LLM: "My Public SDK key is X, my placement ID is Y" so it can generate correct initialization and paywall-fetching code. ::: ### Set up when ready These are not required to start coding, but you'll want them as your integration matures: - **A/B tests**: Configure on the **Placements** page. No code change needed. [A/B tests](ab-tests.md) - **Additional paywalls and placements**: Add more `getPaywall` calls with different placement IDs. - **Analytics integrations**: Configure on the **Integrations** page. Setup varies by integration. See [analytics integrations](analytics-integration.md) and [attribution integrations](attribution-integration.md). ## Feed Adapty docs to your LLM ### Use Context7 (recommended) [Context7](https://context7.com) is an MCP server that gives your LLM direct access to up-to-date Adapty documentation. Your LLM fetches the right docs automatically based on what you ask — no manual URL pasting needed. Context7 works with **Cursor**, **Claude Code**, **Windsurf**, and other MCP-compatible tools. To set it up, run: ``` npx ctx7 setup ``` This detects your editor and configures the Context7 server. For manual setup, see the [Context7 GitHub repository](https://github.com/upstash/context7). Once configured, reference the Adapty library in your prompts: ``` Use the adaptyteam/adapty-docs library to look up how to install the Capacitor SDK ``` :::warning Even though Context7 removes the need to paste doc links manually, the implementation order matters. Follow the [implementation walkthrough](#implementation-walkthrough) below step by step to make sure everything works. ::: ### Use plain text docs You can access any Adapty doc as plain text Markdown. Add `.md` to the end of its URL, or click **Copy for LLM** under the article title. For example: [adapty-cursor-capacitor.md](https://adapty.io/docs/adapty-cursor-capacitor.md). Each stage in the [implementation walkthrough](#implementation-walkthrough) below includes a "Send this to your LLM" block with `.md` links to paste. For more documentation at once, see [index files and platform-specific subsets](#plain-text-doc-index-files) below. ## Implementation walkthrough The rest of this guide walks through Adapty integration in implementation order. Each stage includes the docs to send to your LLM, what you should see when done, and common issues. ### Plan your integration Before jumping into code, ask your LLM to analyze your project and create an implementation plan. If your AI tool supports a planning mode (like Cursor's or Claude Code's plan mode), use it so the LLM can read both your project structure and the Adapty docs before writing any code. Tell your LLM which approach you use for purchases — this affects the guides it should follow: - [**Adapty Paywall Builder**](adapty-paywall-builder.md): You create paywalls in Adapty's no-code builder, and the SDK renders them automatically. - [**Manually created paywalls**](capacitor-making-purchases.md): You build your own paywall UI in code but still use Adapty to fetch products and handle purchases. - [**Observer mode**](observer-vs-full-mode.md): You keep your existing purchase infrastructure and use Adapty only for analytics and integrations. Not sure which one to pick? Read the [comparison table in the quickstart](capacitor-quickstart-paywalls.md). ### Install and configure the SDK Add the Adapty SDK dependency using npm and activate it with your Public SDK key. This is the foundation — nothing else works without it. **Guide:** [Install & configure Adapty SDK](sdk-installation-capacitor.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/sdk-installation-capacitor.md ``` :::tip[Checkpoint] - **Expected:** App builds and runs on both iOS and Android. Console shows Adapty activation log. - **Gotcha:** "Public API key is missing" → check you replaced the placeholder with your real key from App settings. ::: ### Show paywalls and handle purchases Fetch a paywall by placement ID, display it, and handle purchase events. The guides you need depend on how you handle purchases. Test each purchase in the sandbox as you go — don't wait until the end. See [Test purchases in sandbox](test-purchases-in-sandbox.md) for setup instructions. **Guides:** - [Enable purchases using paywalls (quickstart)](capacitor-quickstart-paywalls.md) - [Fetch Paywall Builder paywalls and their configuration](capacitor-get-pb-paywalls.md) - [Display paywalls](capacitor-present-paywalls.md) - [Handle paywall events](capacitor-handling-events.md) - [Respond to button actions](capacitor-handle-paywall-actions.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/capacitor-quickstart-paywalls.md - https://adapty.io/docs/capacitor-get-pb-paywalls.md - https://adapty.io/docs/capacitor-present-paywalls.md - https://adapty.io/docs/capacitor-handling-events.md - https://adapty.io/docs/capacitor-handle-paywall-actions.md ``` :::tip[Checkpoint] - **Expected:** Paywall appears with your configured products. Tapping a product triggers the sandbox purchase dialog. - **Gotcha:** Empty paywall or `getPaywall` error → verify placement ID matches the dashboard exactly and the placement has an audience assigned. ::: **Guides:** - [Enable purchases in your custom paywall (quickstart)](capacitor-quickstart-manual.md) - [Fetch paywalls and products](fetch-paywalls-and-products-capacitor.md) - [Render paywall designed by remote config](present-remote-config-paywalls-capacitor.md) - [Make purchases](capacitor-making-purchases.md) - [Restore purchases](capacitor-restore-purchase.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/capacitor-quickstart-manual.md - https://adapty.io/docs/fetch-paywalls-and-products-capacitor.md - https://adapty.io/docs/present-remote-config-paywalls-capacitor.md - https://adapty.io/docs/capacitor-making-purchases.md - https://adapty.io/docs/capacitor-restore-purchase.md ``` :::tip[Checkpoint] - **Expected:** Your custom paywall displays products fetched from Adapty. Tapping a product triggers the sandbox purchase dialog. - **Gotcha:** Empty products array → verify the paywall has products assigned in the dashboard and the placement has an audience. ::: **Guides:** - [Observer mode overview](observer-vs-full-mode.md) - [Implement Observer mode](implement-observer-mode-capacitor.md) - [Report transactions in Observer mode](report-transactions-observer-mode-capacitor.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/observer-vs-full-mode.md - https://adapty.io/docs/implement-observer-mode-capacitor.md - https://adapty.io/docs/report-transactions-observer-mode-capacitor.md ``` :::tip[Checkpoint] - **Expected:** After a sandbox purchase using your existing purchase flow, the transaction appears in the Adapty dashboard **Event Feed**. - **Gotcha:** No events → verify you're reporting transactions to Adapty and server notifications are configured for both stores. ::: ### Check subscription status After a purchase, check the user profile for an active access level to gate premium content. **Guide:** [Check subscription status](capacitor-check-subscription-status.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/capacitor-check-subscription-status.md ``` :::tip[Checkpoint] - **Expected:** After a sandbox purchase, `profile.accessLevels['premium']?.isActive` returns `true`. - **Gotcha:** Empty `accessLevels` after purchase → check the product has an access level assigned in the dashboard. ::: ### Identify users Link your app user accounts to Adapty profiles so purchases persist across devices. :::important Skip this step if your app has no authentication. ::: **Guide:** [Identify users](capacitor-quickstart-identify.md) Send this to your LLM: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/capacitor-quickstart-identify.md ``` :::tip[Checkpoint] - **Expected:** After calling `adapty.identify()`, the dashboard **Profiles** section shows your custom user ID. - **Gotcha:** Call `identify` after activation but before fetching paywalls to avoid anonymous profile attribution. ::: ### Prepare for release Once your integration works in the sandbox, walk through the release checklist to make sure everything is production-ready. **Guide:** [Release checklist](release-checklist.md) Send this to your LLM: ``` Read these Adapty docs before releasing: - https://adapty.io/docs/release-checklist.md ``` :::tip[Checkpoint] - **Expected:** All checklist items confirmed: store connections, server notifications, purchase flow, access level checks, and privacy requirements. - **Gotcha:** Missing server notifications → configure App Store Server Notifications in **App settings → iOS SDK** and Google Play Real-Time Developer Notifications in **App settings → Android SDK**. ::: ## Plain text doc index files If you need to give your LLM broader context beyond individual pages, we host index files that list or combine all Adapty documentation: - [`llms.txt`](https://adapty.io/docs/llms.txt): Lists all pages with `.md` links. An [emerging standard](https://llmstxt.org/) for making websites accessible to LLMs. Note that for some AI agents (e.g., ChatGPT) you will need to download `llms.txt` and upload it to the chat as a file. - [`llms-full.txt`](https://adapty.io/docs/llms-full.txt): The entire Adapty documentation site combined into a single file. Very large — use only when you need the full picture. - Capacitor-specific [`capacitor-llms.txt`](https://adapty.io/docs/capacitor-llms.txt) and [`capacitor-llms-full.txt`](https://adapty.io/docs/capacitor-llms-full.txt): Platform-specific subsets that save tokens compared to the full site. --- # File: capacitor-paywalls --- --- title: "Paywalls" description: "Learn how to work with paywalls in your Capacitor app with Adapty SDK." --- ## Display paywalls ### Adapty Paywall Builder :::tip To get started with the Adapty Paywall Builder paywalls quickly, see our [quickstart guide](capacitor-quickstart-paywalls). ::: ### Implement paywalls manually For more guides on implementing paywalls and handling purchases manually, see the [category](capacitor-implement-paywalls-manually). ## Useful features --- # File: capacitor-get-pb-paywalls --- --- title: "Fetch Paywall Builder paywalls and their configuration in Capacitor SDK" description: "Learn how to retrieve PB paywalls in Adapty for better subscription control in your Capacitor app." --- After [you designed the visual part for your paywall](adapty-paywall-builder) with the new Paywall Builder in the Adapty Dashboard, you can display it in your mobile app. The first step in this process is to get the paywall associated with the placement and its view configuration as described below. Please be aware that this topic refers to Paywall Builder-customized paywalls. For guidance on fetching remote config paywalls, please refer to the [Fetch paywalls and products for remote config paywalls in your mobile app](fetch-paywalls-and-products-capacitor) topic.
Before you start displaying paywalls in your mobile app (click to expand) 1. [Create your products](create-product) in the Adapty Dashboard. 2. [Create a paywall and incorporate the products into it](create-paywall) in the Adapty Dashboard. 3. [Create placements and incorporate your paywall into it](create-placement) in the Adapty Dashboard. 4. Install [Adapty SDK](sdk-installation-capacitor.md) in your mobile app.
## Fetch paywall designed with Paywall Builder If you've [designed a paywall using the Paywall Builder](adapty-paywall-builder), you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. Nevertheless, you need to get its ID via the placement, its view configuration, and then present it in your mobile app. To ensure optimal performance, it's crucial to retrieve the paywall and its [view configuration](capacitor-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) as early as possible, allowing sufficient time for images to download before presenting them to the user. To get a paywall, use the `getPaywall` method: ```typescript showLineNumbers try { const paywall = await adapty.getPaywall({ placementId: 'YOUR_PLACEMENT_ID', locale: 'en', }); // the requested paywall } catch (error) { // handle the error } ``` Parameters: | Parameter | Presence | Description | |---------|--------|-----------| | **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. | | **locale** |

optional

default: `en`

|

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

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

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

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

optional

default: `en`

|

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

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

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

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

optional

default: `en`

|

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

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

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

| | **params.fetchPolicy** |

optional

default: `'reload_revalidating_cache_data'`

|

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

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

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

| | **params.loadTimeoutMs** |

optional

default: 5000 ms

|

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

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

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

optional

default: `en`

|

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

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

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

| | **params.fetchPolicy** |

optional

default: `'reload_revalidating_cache_data'`

|

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

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

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

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

optional

default: `en`

|

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

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

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

| | **params.fetchPolicy** |

optional

default: `'reload_revalidating_cache_data'`

|

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

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

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

| | **params.loadTimeoutMs** |

optional

default: 5000 ms

|

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

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

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

optional

default: `en`

|

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

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

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

| | **params.fetchPolicy** |

optional

default: `'reload_revalidating_cache_data'`

|

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

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

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

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

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

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

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

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

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

- The user is a minor.

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

The offer identifier is not valid. Possible reasons:

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

- You have revoked the offer.

- You misprinted the offer ID.

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

This error indicates issues with Adapty integration or with offers.

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

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

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

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

2. The user is in an unsupported country.

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

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

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

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

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

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

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

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

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

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

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

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

| | productRequestFailed | 1002 |

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

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

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

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

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

| | productPurchaseFailed | 1006 | Product purchase failed. | | refreshReceiptFailed | 1010 | This error indicates that the receipt was not received. Applicable to StoreKit 1 only. | | receiveRestoredTransactionsFailed | 1011 | Purchase restoration failed. | ## Custom network codes | Error | Code | Description | | :------------------- | :--- | :----------------------------------------------------------- | | notActivated | 2002 | You need to properly configure Adapty SDK by `Adapty.activate` method. Learn how to do it [for React Native](sdk-installation-reactnative#configure-adapty-sdks). | | badRequest | 2003 | Bad request. | | serverError | 2004 | Server error. | | networkFailed | 2005 | The network request failed. | | decodingFailed | 2006 | This error indicates that response decoding failed. | | encodingFailed | 2009 | This error indicates that request encoding failed. | | analyticsDisabled | 3000 | We can't handle analytics events, since you've opted it out. Refer to the [Analytics integration](analytics-integration) topic for details. | | wrongParam | 3001 | This error indicates that some of your parameters are not correct: blank when it cannot be blank or wrong type, etc. | | activateOnceError | 3005 | It is not possible to call `.activate` method more than once. | | profileWasChanged | 3006 | The user profile was changed during the operation. | | fetchTimeoutError | 3101 | This error means that the paywall could not be fetched within the set limit. To avoid this situation, [set up local fallbacks](fetch-paywalls-and-products). | | operationInterrupted | 9000 | This operation was interrupted by the system. | --- # File: capacitor-sdk-migration-guides --- --- title: "Capacitor SDK Migration Guides" description: "Migration guides for Adapty Capacitor SDK versions." --- This page contains all migration guides for Adapty Capacitor SDK. Choose the version you want to migrate to for detailed instructions: - [**Migrate to v. 3.16**](migration-to-capacitor-316.mdx) --- # File: migration-to-capacitor-316 --- --- title: "Migrate Adapty Capacitor SDK to v. 3.16" description: "Migrate to Adapty Capacitor SDK v3.16 for better performance and new monetization features." --- Starting from Adapty SDK v.3.16.0, Capacitor 8 is required. If you need Capacitor 7, use Adapty SDK v.3.15. To upgrade to Capacitor SDK v.3.16, ensure your project uses Capacitor 8. If you're still using Capacitor 7, you have two options: 1. **Upgrade to Capacitor 8**: Follow the [official Capacitor migration guide](https://capacitorjs.com/docs/updating/8-0) to update your project, then install Adapty SDK v.3.16. 2. **Stay on Adapty SDK v.3.15**: If upgrading to Capacitor 8 is not feasible, continue using Adapty SDK v.3.15, which supports Capacitor 7. --- # End of Documentation _Generated on: 2026-02-23T15:34:19.847Z_ _Successfully processed: 37/37 files_