:::important
In a Kotlin Multiplatform project, apply these changes in the Android application module (the one that produces the APK/AAB), for example, `androidApp` or `app`:
- Manifest: `androidApp/src/main/AndroidManifest.xml`
- Backup rules XML: `androidApp/src/main/res/xml/`
:::
#### Purchases fail after returning from another app in Android
If the Activity that starts the purchase flow uses a non-default `launchMode`, Android may recreate or reuse it incorrectly when the user returns from Google Play, a banking app, or a browser. This can cause the purchase result to be lost or treated as canceled.
To ensure purchases work correctly, use only `standard` or `singleTop` launch modes for the Activity that starts the purchase flow, and avoid any other modes.
In your `AndroidManifest.xml`, ensure the Activity that starts the purchase flow is set to `standard` or `singleTop`:
```xml
```
---
# File: kmp-quickstart-paywalls
---
---
title: "Enable purchases by using paywalls in Kotlin Multiplatform SDK"
description: "Quickstart guide to setting up Adapty for in-app subscription management."
---
To enable in-app purchases, you need to understand three key concepts:
- [**Products**](product.md) β anything users can buy (subscriptions, consumables, lifetime access)
- [**Paywalls**](paywalls.md) are configurations that define which products to offer. In Adapty, paywalls are the only way to retrieve products, but this design lets you modify offerings, pricing, and product combinations without touching your app code.
- [**Placements**](placements.md) β where and when you show paywalls in your app (like `main`, `onboarding`, `settings`). You set up paywalls for placements in the dashboard, then request them by placement ID in your code. This makes it easy to run A/B tests and show different paywalls to different users.
Adapty offers you three ways to enable purchases in your app. Select one of them depending on your app requirements:
| Implementation | Complexity | When to use |
|---------------------------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Adapty Paywall Builder | β
Easy | You [create a complete, purchase-ready paywall in the no-code builder](quickstart-paywalls). Adapty automatically renders it and handles all the complex purchase flow, receipt validation, and subscription management behind the scenes. |
| Manually created paywalls | π‘ Medium | You implement your paywall UI in your app code, but still get the paywall object from Adapty to maintain flexibility in product offerings. See the [guide](kmp-quickstart-manual). |
| Observer mode | π΄ Hard | You already have your own purchase handling infrastructure and want to keep using it. Note that the observer mode has its limitations in Adapty. See the [article](observer-vs-full-mode). |
:::important
**The steps below show how to implement a paywall created in the Adapty paywall builder.**
If you don't want to use the paywall builder, see the [guide for handling purchases in manually created paywalls](kmp-making-purchases.md).
:::
To display a paywall created in the Adapty paywall builder, in your app code, you only need to:
1. **Get the paywall**: Get the paywall from Adapty.
2. **Display the paywall and Adapty will handle purchases for you**: Show the paywall container you've got in your app.
3. **Handle button actions**: Associate user interactions with the paywall with your app's response to them. For example, open links or close the paywall when users click buttons.
## Before you start
Before you start, complete these steps:
1. Connect your app to the [App Store](initial_ios) and/or [Google Play](initial-android) in the Adapty Dashboard.
2. [Create your products](create-product) in Adapty.
3. [Create a paywall and add products to it](create-paywall).
4. [Create a placement and add your paywall to it](create-placement).
5. [Install and activate the Adapty SDK](sdk-installation-kotlin-multiplatform) in your app code.
:::tip
The fastest way to complete these steps is to follow the [quickstart guide](quickstart).
:::
## 1. Get the paywall
Your paywalls are associated with placements configured in the dashboard. Placements allow you to run different paywalls for different audiences or to run [A/B tests](ab-tests.md).
To get a paywall created in the Adapty paywall builder, you need to:
1. Get the `paywall` object by the [placement](placements.md) ID using the `getPaywall` method and check whether it is a paywall created in the builder.
2. Get the paywall view configuration using the `createPaywallView` method. The view configuration contains the UI elements and styling needed to display the paywall.
:::important
To get the view configuration, you must switch on the **Show on device** toggle in the Paywall Builder. Otherwise, you will get an empty view configuration, and the paywall won't be displayed.
:::
```kotlin showLineNumbers
Adapty.getPaywall("YOUR_PLACEMENT_ID")
.onSuccess { paywall ->
if (!paywall.hasViewConfiguration) {
return@onSuccess
}
val paywallView = AdaptyUI.createPaywallView(paywall = paywall)
paywallView?.present()
}
.onError { error ->
// handle the error
}
```
## 2. Display the paywall
Now, when you have the paywall configuration, it's enough to add a few lines to display your paywall.
In order to display the visual paywall on the device screen, you must first configure it. To do this, call the method `AdaptyUI.createPaywallView()`:
```kotlin showLineNumbers
val paywallView = AdaptyUI.createPaywallView(paywall = paywall)
paywallView?.present()
```
After the view has been successfully created, you can present it on the screen of the device.
:::tip
For more details on how to display a paywall, see our [guide](kmp-present-paywalls.md).
:::
## 3. Handle button actions
When users click buttons in the paywall, the Kotlin Multiplatform SDK automatically handles purchases, restoration, closing the paywall, and opening links.
However, other buttons have custom or pre-defined IDs and require handling actions in your code. Or, you may want to override their default behavior.
For example, here is the default behavior for the close button. You don't need to add it in the code, but here, you can see how it is done if needed.
:::tip
Read our guides on how to handle button [actions](kmp-handle-paywall-actions.md) and [events](kmp-handling-events.md).
:::
```kotlin showLineNumbers
AdaptyUI.setPaywallsEventsObserver(object : AdaptyUIPaywallsEventsObserver {
override fun paywallViewDidPerformAction(view: AdaptyUIPaywallView, action: AdaptyUIAction) {
when (action) {
AdaptyUIAction.CloseAction, AdaptyUIAction.AndroidSystemBackAction -> view.dismiss()
}
}
})
```
## Next steps
Your paywall is ready to be displayed in the app. Test your purchases in the [App Store sandbox](test-purchases-in-sandbox) or in [Google Play Store](testing-on-android) to make sure you can complete a test purchase from the paywall.
Now, you need to [check the users' access level](kmp-check-subscription-status.md) to ensure you display a paywall or give access to paid features to right users.
## Full example
Here is how all those steps can be integrated in your app together.
```kotlin showLineNumbers
// Set up the observer for handling paywall actions
AdaptyUI.setPaywallsEventsObserver(object : AdaptyUIPaywallsEventsObserver {
override fun paywallViewDidPerformAction(view: AdaptyUIPaywallView, action: AdaptyUIAction) {
when (action) {
is AdaptyUIAction.CloseAction -> view.dismiss()
}
}
})
// Get and display the paywall
Adapty.getPaywall("YOUR_PLACEMENT_ID")
.onSuccess { paywall ->
if (!paywall.hasViewConfiguration) {
// Use custom logic
return@onSuccess
}
val paywallView = AdaptyUI.createPaywallView(paywall = paywall)
paywallView?.present()
}
.onError { error ->
// handle the error
}
```
---
# File: kmp-check-subscription-status
---
---
title: "Check subscription status in Kotlin Multiplatform SDK"
description: "Learn how to check subscription status in your Kotlin Multiplatform app with Adapty."
---
To decide whether users can access paid content or see a paywall, you need to check their [access level](access-level.md) in the profile.
This article shows you how to access the profile state to decide what users need to see - whether to show them a paywall or grant access to paid features.
## Get subscription status
When you decide whether to show a paywall or paid content to a user, you check their [access level](access-level.md) in their profile. You have two options:
- Call `getProfile` if you need the latest profile data immediately (like on app launch) or want to force an update.
- Set up **automatic profile updates** to keep a local copy that's automatically refreshed whenever the subscription status changes.
### Get profile
The easiest way to get the subscription status is to use the `getProfile` method to access the profile:
```kotlin showLineNumbers
Adapty.getProfile()
.onSuccess { profile ->
// check the access
}
.onError { error ->
// handle the error
}
```
### Listen to subscription updates
To automatically receive profile updates in your app:
1. Use `Adapty.setOnProfileUpdatedListener()` to listen for profile changes - Adapty will automatically call this method whenever the user's subscription status changes.
2. Store the updated profile data when this method is called, so you can use it throughout your app without making additional network requests.
```kotlin showLineNumbers
class SubscriptionManager {
private var currentProfile: AdaptyProfile? = null
init {
// Listen for profile updates
Adapty.setOnProfileUpdatedListener { profile ->
currentProfile = profile
// Update UI, unlock content, etc.
}
}
// Use stored profile instead of calling getProfile()
fun hasAccess(): Boolean {
return currentProfile?.accessLevels?.get("YOUR_ACCESS_LEVEL")?.isActive == true
}
}
```
:::note
Adapty automatically calls the profile update listener when your app starts, providing cached subscription data even if the device is offline.
:::
## Connect profile with paywall logic
When you need to make immediate decisions about showing paywalls or granting access to paid features, you can check the user's profile directly. This approach is useful for scenarios like app launch, when entering premium sections, or before displaying specific content.
```kotlin showLineNumbers
private fun checkAccessAndShowPaywall() {
// First, check if user has access
Adapty.getProfile()
.onSuccess { profile ->
val hasAccess = profile.accessLevels?.get("YOUR_ACCESS_LEVEL")?.isActive == true
if (!hasAccess) {
// User doesn't have access, show paywall
showPaywall()
} else {
// User has access, show premium content
showPremiumContent()
}
}
.onError { error ->
// If we can't check access, show paywall as fallback
showPaywall()
}
}
private fun showPaywall() {
// Get and display paywall using the KMP SDK
Adapty.getPaywall("YOUR_PLACEMENT_ID")
.onSuccess { paywall ->
if (paywall.hasViewConfiguration) {
val paywallView = AdaptyUI.createPaywallView(paywall = paywall)
paywallView?.present()
} else {
// Handle remote config paywall or show custom UI
handleRemoteConfigPaywall(paywall)
}
}
.onError { error ->
// Handle paywall loading error
showError("Unable to load paywall")
}
}
private fun showPremiumContent() {
// Show your premium content here
// This is where you unlock paid features
}
```
## Next steps
Now, when you know how to track the subscription status, learn how to [work with user profiles](kmp-quickstart-identify.md) to ensure they can access what they have paid for.
---
# File: kmp-quickstart-identify
---
---
title: "Identify users in Kotlin Multiplatform SDK"
description: "Quickstart guide to setting up Adapty for in-app subscription management in KMP."
---
:::important
This guide is for you if you have your own authentication system. Here, you will learn how to work with user profiles in Adapty to ensure it aligns with your existing authentication system.
:::
How you manage users' purchases depends on your app's authentication model:
- If your app doesn't use backend authentication and doesn't store user data, see the [section about anonymous users](#anonymous-users).
- If your app has (or will have) backend authentication, see the [section about identified users](#identified-users).
**Key concepts**:
- **Profiles** are the entities required for the SDK to work. Adapty creates them automatically.
- They can be anonymous **(without customer user ID)** or identified **(with customer user ID)**.
- You provide **customer user ID** in order to cross-reference profiles in Adapty with your internal auth system
Here is what is different for anonymous and identified users:
| | Anonymous users | Identified users |
|-------------------------|---------------------------------------------------|-------------------------------------------------------------------------|
| **Purchase management** | Store-level purchase restoration | Maintain purchase history across devices through their customer user ID |
| **Profile management** | New profiles on each reinstall | The same profile across sessions and devices |
| **Data persistence** | Anonymous users' data is tied to app installation | Identified users' data persists across app installations |
## Anonymous users
If you don't have backend authentication, **you don't need to handle authentication in the app code**:
1. When the SDK is activated on the app's first launch, Adapty **creates a new profile for the user**.
2. When the user purchases anything in the app, this purchase is **associated with their Adapty profile and their store account**.
3. When the user **re-installs** the app or installs it from a **new device**, Adapty **creates a new anonymous profile on activation**.
4. If the user has previously made purchases in your app, by default, their purchases are automatically synced from the App Store on the SDK activation.
So, with anonymous users, new profiles will be created on each installation, but that's not a problem because, in the Adapty analytics, you can [configure what will be considered a new installation](general#4-installs-definition-for-analytics).
For anonymous users, you need to count installs by **device IDs**. In this case, each app installation on a device is counted as an install, including reinstalls.
:::note
Backup restores behave differently from reinstalls. By default, when a user restores from a backup, the SDK preserves cached data and does not create a new profile. You can configure this behavior using the `withAppleClearDataOnBackup` setting. [Learn more](sdk-installation-kotlin-multiplatform#clear-data-on-backup-restore).
:::
## Identified users
You have two options to identify users in the app:
- [**During login/signup:**](#during-loginsignup) If users sign in after your app starts, call `identify()` with a customer user ID when they authenticate.
- [**During the SDK activation:**](#during-the-sdk-activation) If you already have a customer user ID stored when the app launches, send it when calling `activate()`.
:::important
By default, when Adapty receives a purchase from a Customer User ID that is currently associated with another Customer User ID, the access level is shared, so both profiles have paid access. You can configure this setting to transfer paid access from one profile to another or disable sharing completely. See the [article](general#6-sharing-purchases-between-user-accounts) for more details.
:::
### During login/signup
If you're identifying users after the app launch (for example, after they log into your app or sign up), use the `identify` method to set their customer user ID.
- If you **haven't used this customer user ID before**, Adapty will automatically link it to the current profile.
- If you **have used this customer user ID to identify the user before**, Adapty will switch to working with the profile associated with this customer user ID.
:::important
Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one.
:::
```kotlin showLineNumbers
Adapty.identify("YOUR_USER_ID") // Unique for each user
.onSuccess {
// successful identify
}
.onError { error ->
// handle the error
}
```
### During the SDK activation
If you already know a customer user ID when you activate the SDK, you can send it in the `activate` method instead of calling `identify` separately.
If you know a customer user ID but set it only after the activation, that will mean that, upon activation, Adapty will create a new anonymous profile and switch to the existing one only after you call `identify`.
You can pass either an existing customer user ID (the one you have used before) or a new one. If you pass a new one, a new profile created upon activation will be automatically linked to the customer user ID.
:::note
By default, creating anonymous profiles does not affect analytics dashboards, because installs are counted based on device IDs.
A device ID represents a single installation of the app from the store on a device and is regenerated only after the app is reinstalled.
It does not depend on whether this is a first or repeated installation, or whether an existing customer user ID is used.
Creating a profile (on SDK activation or logout), logging in, or upgrading the app without reinstalling the app does not generate additional install events.
If you want to count installs based on unique users rather than devices, go to **App settings** and configure [**Installs definition for analytics**](general#4-installs-definition-for-analytics).
:::
```kotlin showLineNumbers
AdaptyConfig.Builder("PUBLIC_SDK_KEY")
.withCustomerUserId("user123") // Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one.
.build()
```
### Log users out
If you have a button for logging users out, use the `logout` method.
:::important
Logging users out creates a new anonymous profile for the user.
:::
```kotlin showLineNumbers
Adapty.logout()
.onSuccess {
// successful logout
}
.onError { error ->
// handle the error
}
```
:::info
To log users back into the app, use the `identify` method.
:::
### Allow purchases without login
If your users can make purchases both before and after they log into your app, you need to ensure that they will keep access after they log in:
1. When a logged-out user makes a purchase, Adapty ties it to their anonymous profile ID.
2. When the user logs into their account, Adapty switches to working with their identified profile.
- If it is a new customer user ID (e.g., the purchase has been made before registration), Adapty assigns the customer user ID to the current profile, so all the purchase history is maintained.
- If it is an existing customer user ID (the customer user ID is already linked to a profile), you need to get the actual access level after the profile switch. You can either call [`getProfile`](kmp-check-subscription-status.md) right after the identification, or [listen for profile updates](kmp-check-subscription-status.md) so the data syncs automatically.
## Next steps
Congratulations! You have implemented in-app payment logic in your app! We wish you all the best with your app monetization!
To get even more from Adapty, you can explore these topics:
- [**Testing**](troubleshooting-test-purchases.md): Ensure that everything works as expected
- [**Integrations**](configuration.md): Integrate with marketing attribution and analytics services in just one line of code
- [**Set custom profile attributes**](kmp-setting-user-attributes.md): Add custom attributes to user profiles and create segments, so you can launch A/B tests or show different paywalls to different users
---
# File: adapty-cursor-kmp
---
---
title: "Integrate Adapty into your Kotlin Multiplatform app with AI assistance"
description: "A step-by-step guide to integrating Adapty into your Kotlin Multiplatform app using Cursor, Context7, ChatGPT, Claude, or other AI tools."
---
This guide helps you integrate Adapty into your Kotlin Multiplatform app with the help of an LLM. You'll start by preparing your Adapty dashboard, then work through each implementation stage by sending focused doc links to your LLM. At the end, you'll find best practices for setting up your AI tools with Adapty documentation.
:::tip
Copy this entire page as Markdown and paste it into your LLM to get started β click **Copy for LLM** at the top of the page or open [the .md version](https://adapty.io/docs/adapty-cursor-kmp.md). The LLM will use the guide links and checkpoints to walk you through each stage.
:::
## Before you start: dashboard checklist
Adapty requires dashboard configuration before you write any SDK code. Your LLM cannot look up dashboard values for you β you'll need to provide them.
### Required before coding
1. **Connect your app stores**: In the Adapty Dashboard, go to **App settings β General**. Connect both App Store and Google Play if your KMP app targets both platforms. This is required for purchases to work.
[Connect app stores](integrate-payments.md)
2. **Copy your Public SDK key**: In the Adapty Dashboard, go to **App settings β General**, then find the **API keys** section. In code, this is the string you pass to the Adapty configuration builder.
3. **Create at least one product**: In the Adapty Dashboard, go to the **Products** page. You don't reference products directly in code β Adapty delivers them through paywalls.
[Add products](quickstart-products.md)
4. **Create a paywall and a placement**: In the Adapty Dashboard, create a paywall on the **Paywalls** page, then assign it to a placement on the **Placements** page. In code, the placement ID is the string you pass to `Adapty.getPaywall("YOUR_PLACEMENT_ID")`.
[Create paywall](quickstart-paywalls.md)
5. **Set up access levels**: In the Adapty Dashboard, configure per product on the **Products** page. In code, the string checked in `profile.accessLevels["premium"]?.isActive`. The default `premium` access level works for most apps. If paying users get access to different features depending on the product (for example, a `basic` plan vs. a `pro` plan), [create additional access levels](assigning-access-level-to-a-product.md) before you start coding.
:::tip
Once you have all five, you're ready to write code. Tell your LLM: "My Public SDK key is X, my placement ID is Y" so it can generate correct initialization and paywall-fetching code.
:::
### Set up when ready
These are not required to start coding, but you'll want them as your integration matures:
- **A/B tests**: Configure on the **Placements** page. No code change needed.
[A/B tests](ab-tests.md)
- **Additional paywalls and placements**: Add more `getPaywall` calls with different placement IDs.
- **Analytics integrations**: Configure on the **Integrations** page. Setup varies by integration. See [analytics integrations](analytics-integration.md) and [attribution integrations](attribution-integration.md).
## Feed Adapty docs to your LLM
### Use Context7 (recommended)
[Context7](https://context7.com) is an MCP server that gives your LLM direct access to up-to-date Adapty documentation. Your LLM fetches the right docs automatically based on what you ask β no manual URL pasting needed.
Context7 works with **Cursor**, **Claude Code**, **Windsurf**, and other MCP-compatible tools. To set it up, run:
```
npx ctx7 setup
```
This detects your editor and configures the Context7 server. For manual setup, see the [Context7 GitHub repository](https://github.com/upstash/context7).
Once configured, reference the Adapty library in your prompts:
```
Use the adaptyteam/adapty-docs library to look up how to install the Kotlin Multiplatform SDK
```
:::warning
Even though Context7 removes the need to paste doc links manually, the implementation order matters. Follow the [implementation walkthrough](#implementation-walkthrough) below step by step to make sure everything works.
:::
### Use plain text docs
You can access any Adapty doc as plain text Markdown. Add `.md` to the end of its URL, or click **Copy for LLM** under the article title. For example: [adapty-cursor-kmp.md](https://adapty.io/docs/adapty-cursor-kmp.md).
Each stage in the [implementation walkthrough](#implementation-walkthrough) below includes a "Send this to your LLM" block with `.md` links to paste.
For more documentation at once, see [index files and platform-specific subsets](#plain-text-doc-index-files) below.
## Implementation walkthrough
The rest of this guide walks through Adapty integration in implementation order. Each stage includes the docs to send to your LLM, what you should see when done, and common issues.
### Plan your integration
Before jumping into code, ask your LLM to analyze your project and create an implementation plan. If your AI tool supports a planning mode (like Cursor's or Claude Code's plan mode), use it so the LLM can read both your project structure and the Adapty docs before writing any code.
Tell your LLM which approach you use for purchases β this affects the guides it should follow:
- [**Adapty Paywall Builder**](adapty-paywall-builder.md): You create paywalls in Adapty's no-code builder, and the SDK renders them automatically.
- [**Manually created paywalls**](kmp-making-purchases.md): You build your own paywall UI in code but still use Adapty to fetch products and handle purchases.
- [**Observer mode**](observer-vs-full-mode.md): You keep your existing purchase infrastructure and use Adapty only for analytics and integrations.
Not sure which one to pick? Read the [comparison table in the quickstart](kmp-quickstart-paywalls.md).
### Install and configure the SDK
Add the Adapty SDK dependency via Gradle and activate it with your Public SDK key. This is the foundation β nothing else works without it.
**Guide:** [Install & configure Adapty SDK](sdk-installation-kotlin-multiplatform.md)
Send this to your LLM:
```
Read these Adapty docs before writing code:
- https://adapty.io/docs/sdk-installation-kotlin-multiplatform.md
```
:::tip[Checkpoint]
- **Expected:** App builds and runs. Logcat (Android) or Xcode console (iOS) shows Adapty activation log.
- **Gotcha:** "Public API key is missing" β check you replaced the placeholder with your real key from App settings.
:::
### Show paywalls and handle purchases
Fetch a paywall by placement ID, display it, and handle purchase events. The guides you need depend on how you handle purchases.
Test each purchase in the sandbox as you go β don't wait until the end. See [Test purchases in sandbox](test-purchases-in-sandbox.md) for setup instructions.
**Guides:**
- [Enable purchases using paywalls (quickstart)](kmp-quickstart-paywalls.md)
- [Fetch Paywall Builder paywalls and their configuration](kmp-get-pb-paywalls.md)
- [Display paywalls](kmp-present-paywalls.md)
- [Handle paywall events](kmp-handling-events.md)
- [Respond to button actions](kmp-handle-paywall-actions.md)
Send this to your LLM:
```
Read these Adapty docs before writing code:
- https://adapty.io/docs/kmp-quickstart-paywalls.md
- https://adapty.io/docs/kmp-get-pb-paywalls.md
- https://adapty.io/docs/kmp-present-paywalls.md
- https://adapty.io/docs/kmp-handling-events.md
- https://adapty.io/docs/kmp-handle-paywall-actions.md
```
:::tip[Checkpoint]
- **Expected:** Paywall appears with your configured products. Tapping a product triggers the sandbox purchase dialog.
- **Gotcha:** Empty paywall or `getPaywall` error β verify placement ID matches the dashboard exactly and the placement has an audience assigned.
:::
**Guides:**
- [Enable purchases in your custom paywall (quickstart)](kmp-quickstart-manual.md)
- [Fetch paywalls and products](fetch-paywalls-and-products-kmp.md)
- [Render paywall designed by remote config](present-remote-config-paywalls-kmp.md)
- [Make purchases](kmp-making-purchases.md)
- [Restore purchases](kmp-restore-purchase.md)
Send this to your LLM:
```
Read these Adapty docs before writing code:
- https://adapty.io/docs/kmp-quickstart-manual.md
- https://adapty.io/docs/fetch-paywalls-and-products-kmp.md
- https://adapty.io/docs/present-remote-config-paywalls-kmp.md
- https://adapty.io/docs/kmp-making-purchases.md
- https://adapty.io/docs/kmp-restore-purchase.md
```
:::tip[Checkpoint]
- **Expected:** Your custom paywall displays products fetched from Adapty. Tapping a product triggers the sandbox purchase dialog.
- **Gotcha:** Empty products array β verify the paywall has products assigned in the dashboard and the placement has an audience.
:::
**Guides:**
- [Observer mode overview](observer-vs-full-mode.md)
- [Implement Observer mode](implement-observer-mode-kmp.md)
- [Report transactions in Observer mode](report-transactions-observer-mode-kmp.md)
Send this to your LLM:
```
Read these Adapty docs before writing code:
- https://adapty.io/docs/observer-vs-full-mode.md
- https://adapty.io/docs/implement-observer-mode-kmp.md
- https://adapty.io/docs/report-transactions-observer-mode-kmp.md
```
:::tip[Checkpoint]
- **Expected:** After a sandbox purchase using your existing purchase flow, the transaction appears in the Adapty dashboard **Event Feed**.
- **Gotcha:** No events β verify you're reporting transactions to Adapty and server notifications are configured for both stores.
:::
### Check subscription status
After a purchase, check the user profile for an active access level to gate premium content.
**Guide:** [Check subscription status](kmp-check-subscription-status.md)
Send this to your LLM:
```
Read these Adapty docs before writing code:
- https://adapty.io/docs/kmp-check-subscription-status.md
```
:::tip[Checkpoint]
- **Expected:** After a sandbox purchase, `profile.accessLevels["premium"]?.isActive` returns `true`.
- **Gotcha:** Empty `accessLevels` after purchase β check the product has an access level assigned in the dashboard.
:::
### Identify users
Link your app user accounts to Adapty profiles so purchases persist across devices.
:::important
Skip this step if your app has no authentication.
:::
**Guide:** [Identify users](kmp-quickstart-identify.md)
Send this to your LLM:
```
Read these Adapty docs before writing code:
- https://adapty.io/docs/kmp-quickstart-identify.md
```
:::tip[Checkpoint]
- **Expected:** After calling `Adapty.identify("your-user-id")`, the dashboard **Profiles** section shows your custom user ID.
- **Gotcha:** Call `identify` after activation but before fetching paywalls to avoid anonymous profile attribution.
:::
### Prepare for release
Once your integration works in the sandbox, walk through the release checklist to make sure everything is production-ready.
**Guide:** [Release checklist](release-checklist.md)
Send this to your LLM:
```
Read these Adapty docs before releasing:
- https://adapty.io/docs/release-checklist.md
```
:::tip[Checkpoint]
- **Expected:** All checklist items confirmed: store connections, server notifications, purchase flow, access level checks, and privacy requirements.
- **Gotcha:** Missing server notifications β configure App Store Server Notifications in **App settings β iOS SDK** and Google Play Real-Time Developer Notifications in **App settings β Android SDK**.
:::
## Plain text doc index files
If you need to give your LLM broader context beyond individual pages, we host index files that list or combine all Adapty documentation:
- [`llms.txt`](https://adapty.io/docs/llms.txt): Lists all pages with `.md` links. An [emerging standard](https://llmstxt.org/) for making websites accessible to LLMs. Note that for some AI agents (e.g., ChatGPT) you will need to download `llms.txt` and upload it to the chat as a file.
- [`llms-full.txt`](https://adapty.io/docs/llms-full.txt): The entire Adapty documentation site combined into a single file. Very large β use only when you need the full picture.
- Kotlin Multiplatform-specific [`kmp-llms.txt`](https://adapty.io/docs/kmp-llms.txt) and [`kmp-llms-full.txt`](https://adapty.io/docs/kmp-llms-full.txt): Platform-specific subsets that save tokens compared to the full site.
---
# File: kmp-paywalls
---
---
title: "Paywalls in Kotlin Multiplatform SDK"
description: "Learn how to work with paywalls in your Kotlin Multiplatform app with Adapty SDK."
---
## Display paywalls
### Adapty Paywall Builder
:::tip
To get started with the Adapty Paywall Builder paywalls quickly, see our [quickstart guide](kmp-quickstart-paywalls).
:::
### Implement paywalls manually
For more guides on implementing paywalls and handling purchases manually, see the [category](kmp-implement-paywalls-manually).
## Useful features
---
# File: kmp-get-pb-paywalls
---
---
title: "Fetch Paywall Builder paywalls and their configuration in Kotlin Multiplatform SDK"
description: "Learn how to retrieve PB paywalls in Adapty for better subscription control in your Kotlin Multiplatform app."
---
After [you designed the visual part for your paywall](adapty-paywall-builder) with the new Paywall Builder in the Adapty Dashboard, you can display it in your mobile app. The first step in this process is to get the paywall associated with the placement and its view configuration as described below.
Please be aware that this topic refers to Paywall Builder-customized paywalls. If you are implementing your paywalls manually, please refer to the [Fetch paywalls and products for remote config paywalls in your mobile app](fetch-paywalls-and-products-kmp) topic.
:::tip
Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality.
:::
Before you start displaying paywalls in your mobile app (click to expand)
1. [Create your products](create-product) in the Adapty Dashboard.
2. [Create a paywall and incorporate the products into it](create-paywall) in the Adapty Dashboard.
3. [Create placements and incorporate your paywall into it](create-placement) in the Adapty Dashboard.
4. Install [Adapty SDK](sdk-installation-kotlin-multiplatform) in your mobile app.
## Fetch paywall designed with Paywall Builder
If you've [designed a paywall using the Paywall Builder](adapty-paywall-builder), you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. Nevertheless, you need to get its ID via the placement, its view configuration, and then present it in your mobile app.
To ensure optimal performance, it's crucial to retrieve the paywall and its [view configuration](kmp-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) as early as possible, allowing sufficient time for images to download before presenting them to the user.
To get a paywall, use the `getPaywall` method:
```kotlin showLineNumbers
Adapty.getPaywall(
placementId = "YOUR_PLACEMENT_ID",
locale = "en",
fetchPolicy = AdaptyPaywallFetchPolicy.Default,
loadTimeout = 5.seconds
).onSuccess { paywall ->
// the requested paywall
}.onError { error ->
// handle the error
}
```
Parameters:
| Parameter | Presence | Description |
|---------|--------|-----------|
| **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. |
| **locale** | optional
default: `en`
| The identifier of the [paywall localization](add-paywall-locale-in-adapty-paywall-builder). This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
|
| **fetchPolicy** | default: `AdaptyPaywallFetchPolicy.Default` | By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `AdaptyPaywallFetchPolicy.ReturnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores paywalls locally in two layers: regularly updated cache described above and [fallback paywalls](fallback-paywalls). We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.
|
| **loadTimeout** | default: 5 sec | This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.
Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.
For Kotlin Multiplatform: You can create `TimeInterval` with extension functions (like `5.seconds`, where `.seconds` is from `import com.adapty.utils.seconds`), or `TimeInterval.seconds(5)`. To set no limitation, use `TimeInterval.INFINITE`.
|
Response parameters:
| Parameter | Description |
| :-------- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Paywall | An [`AdaptyPaywall`](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-paywall/) object with a list of product IDs, the paywall identifier, remote config, and several other properties. |
## Fetch the view configuration of paywall designed using Paywall Builder
:::important
Make sure to enable the **Show on device** toggle in the paywall builder. If this option isn't turned on, the view configuration won't be available to retrieve.
:::
After fetching the paywall, check if it includes a `ViewConfiguration`, which indicates that it was created using Paywall Builder. This will guide you on how to display the paywall. If the `ViewConfiguration` is present, treat it as a Paywall Builder paywall; if not, [handle it as a remote config paywall](present-remote-config-paywalls-kmp).
Use the `createPaywallView` method to load the view configuration.
```kotlin showLineNumbers
if (paywall.hasViewConfiguration) {
AdaptyUI.createPaywallView(
paywall = paywall,
loadTimeout = 5.seconds,
preloadProducts = true
).onSuccess { paywallView ->
// use paywallView
}.onError { error ->
// handle the error
}
} else {
// use your custom logic
}
```
| Parameter | Presence | Description |
| :--------------------------- | :------------- | :----------------------------------------------------------- |
| **paywall** | required | An `AdaptyPaywall` object to obtain a controller for the desired paywall. |
| **loadTimeout** | optional | This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned. Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood. You can use extension functions like `5.seconds` from `kotlin.time.Duration.Companion`. |
| **preloadProducts** | optional | Set to `true` to preload products for better performance. When enabled, products are loaded in advance, reducing the time needed to display the paywall. |
| **productPurchaseParams** | optional | A map of [`AdaptyProductIdentifier`](https://kmp.adapty.io/adapty/com.adapty.kmp.models/-adapty-product-identifier/) to [`AdaptyPurchaseParameters`](https://kmp.adapty.io/adapty/com.adapty.kmp.models/-adapty-purchase-parameters/). Use this to configure purchase-specific parameters like personalized offers or subscription update parameters for individual products in the paywall. |
:::note
If you are using multiple languages, learn how to add a [Paywall Builder localization](add-paywall-locale-in-adapty-paywall-builder).
:::
Once loaded, [present the paywall](kmp-present-paywalls).
## Get a paywall for a default audience to fetch it faster
Typically, paywalls are fetched almost instantly, so you don't need to worry about speeding up this process. However, in cases where you have numerous audiences and paywalls, and your users have a weak internet connection, fetching a paywall may take longer than you'd like. In such situations, you might want to display a default paywall to ensure a smooth user experience rather than showing no paywall at all.
To address this, you can use the `getPaywallForDefaultAudience` method, which fetches the paywall of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the paywall by the `getPaywall` method, as detailed in the [Fetch Paywall Information](#fetch-paywall-designed-with-paywall-builder) section above.
:::warning
Why we recommend using `getPaywall`
The `getPaywallForDefaultAudience` method comes with a few significant drawbacks:
- **Potential backward compatibility issues**: If you need to show different paywalls for different app versions (current and future), you may face challenges. You'll either have to design paywalls that support the current (legacy) version or accept that users with the current (legacy) version might encounter issues with non-rendered paywalls.
- **Loss of targeting**: All users will see the same paywall designed for the **All Users** audience, which means you lose personalized targeting (including based on countries, marketing attribution or your own custom attributes).
If you're willing to accept these drawbacks to benefit from faster paywall fetching, use the `getPaywallForDefaultAudience` method as follows. Otherwise stick to `getPaywall` described [above](#fetch-paywall-designed-with-paywall-builder).
:::
```kotlin showLineNumbers
Adapty.getPaywallForDefaultAudience(
placementId = "YOUR_PLACEMENT_ID",
locale = "en",
fetchPolicy = AdaptyPaywallFetchPolicy.Default,
).onSuccess { paywall ->
// the requested paywall
}.onError { error ->
// handle the error
}
```
| Parameter | Presence | Description |
|---------|--------|-----------|
| **placementId** | required | The identifier of the [Placement](placements). This is the value you specified when creating a placement in your Adapty Dashboard. |
| **locale** | optional
default: `en`
| The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
|
| **fetchPolicy** | default: `AdaptyPaywallFetchPolicy.Default` | By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `AdaptyPaywallFetchPolicy.ReturnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
|
## Customize assets
To customize images and videos in your paywall, implement the custom assets.
Hero images and videos have predefined IDs: `hero_image` and `hero_video`. In a custom asset bundle, you target these elements by their IDs and customize their behavior.
For other images and videos, you need to [set a custom ID](https://adapty.io/docs/custom-media) in the Adapty dashboard.
For example, you can:
- Show a different image or video to some users.
- Show a local preview image while a remote main image is loading.
- Show a preview image before running a video.
:::important
To use this feature, update the Adapty SDK to version 3.7.0 or higher.
:::
Here's an example of how you can provide custom assets via a map:
:::info
The Kotlin Multiplatform SDK supports local assets only. For remote content, you should download and cache assets locally before using them in custom assets.
:::
```kotlin showLineNumbers
// Import generated Res class for accessing resources
viewModelScope.launch {
// Get URIs for bundled resources using Res.getUri()
val heroImagePath = Res.getUri("files/images/hero_image.png")
val demoVideoPath = Res.getUri("files/videos/demo_video.mp4")
// Or read image as byte data
val imageByteData = Res.readBytes("files/images/avatar.png")
// Create custom assets map
val customAssets: Map = mapOf(
// Load image from app resources (bundled with the app)
// Files should be placed in commonMain/composeResources/files/
"hero_image" to AdaptyCustomAsset.localImageResource(
path = heroImagePath
),
// Or use image byte data
"avatar" to AdaptyCustomAsset.localImageData(
data = imageByteData
),
// Load video from app resources
"demo_video" to AdaptyCustomAsset.localVideoResource(
path = demoVideoPath
),
// Or use a video file from device storage
"intro_video" to AdaptyCustomAsset.localVideoFile(
path = "/path/to/local/video.mp4"
),
// Apply custom brand colors
"brand_primary" to AdaptyCustomAsset.color(
colorHex = "#FF6B35"
),
// Create gradient background
"card_gradient" to AdaptyCustomAsset.linearGradient(
colors = listOf("#1E3A8A", "#3B82F6", "#60A5FA"),
stops = listOf(0.0f, 0.5f, 1.0f)
)
)
// Use custom assets when creating paywall view
AdaptyUI.createPaywallView(
paywall = paywall,
customAssets = customAssets
).onSuccess { paywallView ->
// Present the paywall with custom assets
paywallView.present()
}.onError { error ->
// Handle the error - paywall will fall back to default appearance
}
}
```
:::note
If an asset is not found or fails to load, the paywall will fall back to its default appearance configured in the Paywall Builder.
:::
---
# File: kmp-present-paywalls
---
---
title: "Kotlin Multiplatform - Present new Paywall Builder paywalls"
description: "Learn how to present paywalls on Kotlin Multiplatform for effective monetization."
---
If you've customized a paywall using the Paywall Builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown.
:::warning
This guide is for **new Paywall Builder paywalls** only. The process for presenting paywalls differs for paywalls designed with remote config paywalls and [Observer mode](observer-vs-full-mode).
For presenting **Remote config paywalls**, see [Render paywall designed by remote config](present-remote-config-paywalls-kmp).
:::
To display a paywall, use the `view.present()` method on the `view` created by the [`createPaywallView`](kmp-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) method. Each `view` can only be used once. If you need to display the paywall again, call `createPaywallView` one more to create a new `view` instance.
:::warning
Reusing the same `view` without recreating it may result in an error.
:::
```kotlin showLineNumbers title="Kotlin Multiplatform"
viewModelScope.launch {
AdaptyUI.createPaywallView(paywall = paywall).onSuccess { view ->
view.present()
}.onError { error ->
// handle the error
}
}
```
## Custom tags
Custom tags let you avoid creating separate paywalls for different scenarios. Imagine a single paywall that adapts dynamically based on user data. For example, instead of a generic "Hello!", you could greet users personally with "Hello, John!" or "Hello, Ann!"
Here are some ways you can use custom tags:
- Display the user's name or email on the paywall.
- Show the current day of the week to boost sales (e.g., "Happy Thursday").
- Add personalized details about the products you're selling (like the name of a fitness program or a phone number in a VoIP app).
Custom tags help you create a flexible paywall that adapts to various situations, making your app's interface more personalized and engaging.
:::warning
Make sure to add fallbacks for every line with custom tags.
Remember to include fallbacks for every line with custom tags.
In some cases, your app might not know what to replace a custom tag withβespecially if users are on an older version of the AdaptyUI SDK. To prevent this, always add fallback text that will replace lines containing unknown custom tags. Without this, users might see the tags displayed as code (``).
:::
To use custom tags in your paywall, pass them when creating the paywall view:
```kotlin showLineNumbers
viewModelScope.launch {
val customTags = mapOf(
"USERNAME" to "John",
"DAY_OF_WEEK" to "Thursday"
)
AdaptyUI.createPaywallView(
paywall = paywall,
customTags = customTags
).onSuccess { view ->
view.present()
}.onError { error ->
// handle the error
}
}
```
## Custom timers
The paywall timer is a great tool for promoting special and seasonal offers with a time limit. However, it's important to note that this timer isn't connected to the offer's validity or the campaign's duration. It's simply a standalone countdown that starts from the value you set and decreases to zero. When the timer reaches zero, nothing happensβit just stays at zero.
You can customize the text before and after the timer to create the desired message, such as: "Offer ends in: 10:00 sec."
To use custom timers in your paywall, pass them when creating the paywall view:
```kotlin showLineNumbers
viewModelScope.launch {
val customTimers = mapOf(
"CUSTOM_TIMER_NY" to LocalDateTime(2025, 1, 1, 0, 0, 0),
"CUSTOM_TIMER_SALE" to LocalDateTime(2024, 12, 31, 23, 59, 59)
)
AdaptyUI.createPaywallView(
paywall = paywall,
customTimers = customTimers
).onSuccess { view ->
view.present()
}.onError { error ->
// handle the error
}
}
```
## Show dialog
Use this method instead of native alert dialogs when a paywall view is presented on Android. On Android, regular alerts appear behind the paywall view, which makes them invisible to users. This method ensures proper dialog presentation above the paywall on all platforms.
```kotlin showLineNumbers title="Kotlin Multiplatform"
viewModelScope.launch {
view.showDialog(
title = "Close paywall?",
content = "You will lose access to exclusive offers.",
primaryActionTitle = "Stay",
secondaryActionTitle = "Close"
).onSuccess { action ->
if (action == AdaptyUIDialogActionType.SECONDARY) {
// User confirmed - close the paywall
view.dismiss()
}
// If primary - do nothing, user stays
}.onError { error ->
// handle the error
}
}
```
## Configure iOS presentation style
Configure how the paywall is presented on iOS by passing the `iosPresentationStyle` parameter to the `present()` method. The parameter accepts `AdaptyUIIOSPresentationStyle.FULLSCREEN` (default) or `AdaptyUIIOSPresentationStyle.PAGESHEET` values.
```kotlin showLineNumbers
viewModelScope.launch {
val view = AdaptyUI.createPaywallView(paywall = paywall).getOrNull()
view?.present(iosPresentationStyle = AdaptyUIIOSPresentationStyle.PAGESHEET)
}
```
---
# File: kmp-handle-paywall-actions
---
---
title: "Respond to button actions in Kotlin Multiplatform SDK"
description: "Handle paywall button actions in Kotlin Multiplatform using Adapty for better app monetization."
---
:::warning
**Only purchases and restorations are handled automatically.** All the other button actions, such as closing paywalls or opening links, require implementing proper responses in the app code.
:::
If you are building paywalls using the Adapty paywall builder, it's crucial to set up buttons properly:
1. Add a [button in the paywall builder](paywall-buttons.md) and assign it either a pre-existing action or create a custom action ID.
2. Write code in your app to handle each action you've assigned.
This guide shows how to handle custom and pre-existing actions in your code.
## Set up the AdaptyUIPaywallsEventsObserver
To handle paywall actions, you need to implement the `AdaptyUIPaywallsEventsObserver` interface and set it up with `AdaptyUI.setPaywallsEventsObserver()`. This should be done early in your app's lifecycle, typically in your main activity or app initialization.
```kotlin
// In your app initialization
AdaptyUI.setPaywallsEventsObserver(MyAdaptyUIPaywallsEventsObserver())
```
## Close paywalls
To add a button that will close your paywall:
1. In the paywall builder, add a button and assign it the **Close** action.
2. In your app code, implement a handler for the `close` action that dismisses the paywall.
:::info
In the Kotlin Multiplatform SDK, the `CloseAction` and `AndroidSystemBackAction` trigger closing the paywall by default. However, you can override this behavior in your code if needed. For example, closing one paywall might trigger opening another.
:::
```kotlin
class MyAdaptyUIPaywallsEventsObserver : AdaptyUIPaywallsEventsObserver {
override fun paywallViewDidPerformAction(view: AdaptyUIPaywallView, action: AdaptyUIAction) {
when (action) {
AdaptyUIAction.CloseAction, AdaptyUIAction.AndroidSystemBackAction -> view.dismiss()
}
}
}
// Set up the observer
AdaptyUI.setPaywallsEventsObserver(MyAdaptyUIPaywallsEventsObserver())
```
## Open URLs from paywalls
:::tip
If you want to add a group of links (e.g., terms of use and purchase restoration), add a **Link** element in the paywall builder and handle it the same way as buttons with the **Open URL** action.
:::
To add a button that opens a link from your paywall (e.g., **Terms of use** or **Privacy policy**):
1. In the paywall builder, add a button, assign it the **Open URL** action, and enter the URL you want to open.
2. In your app code, implement a handler for the `openUrl` action that opens the received URL in a browser.
:::info
In the Kotlin Multiplatform SDK, the `OpenUrlAction` provides the URL that should be opened. You can implement custom logic to handle URL opening, such as showing a confirmation dialog or using your app's preferred URL handling method.
:::
```kotlin
class MyAdaptyUIPaywallsEventsObserver(
private val uriHandler: UriHandler
) : AdaptyUIPaywallsEventsObserver {
override fun paywallViewDidPerformAction(view: AdaptyUIPaywallView, action: AdaptyUIAction) {
when (action) {
is AdaptyUIAction.OpenUrlAction -> {
// Show confirmation dialog before opening URL
mainUiScope.launch {
val selectedAction = view.showDialog(
title = "Open URL?",
content = action.url,
primaryActionTitle = "Cancel",
secondaryActionTitle = "Open"
).getOrNull()
when (selectedAction) {
AdaptyUIDialogActionType.PRIMARY -> {
// User cancelled
}
AdaptyUIDialogActionType.SECONDARY -> {
// User confirmed - open URL
uriHandler.openUri(action.url)
}
else -> Unit
}
}
}
}
}
}
// Set up the observer with UriHandler
AdaptyUI.setPaywallsEventsObserver(MyAdaptyUIPaywallsEventsObserver(uriHandler))
```
## Log into the app
To add a button that logs users into your app:
1. In the paywall builder, add a button and assign it a **Custom** action with the ID "login".
2. In your app code, implement a handler for the custom action that identifies your user.
```kotlin
class MyAdaptyUIObserver : AdaptyUIObserver {
override fun paywallViewDidPerformAction(view: AdaptyUIView, action: AdaptyUIAction) {
when (action) {
is AdaptyUIAction.CustomAction -> {
if (action.action == "login") {
// Handle login action - navigate to login screen
// This depends on your app's navigation system
// For example, in Compose Multiplatform:
// navController.navigate("login")
}
}
}
}
}
```
## Handle custom actions
To add a button that handles any other actions:
1. In the paywall builder, add a button, assign it the **Custom** action, and assign it an ID.
2. In your app code, implement a handler for the action ID you've created.
For example, if you have another set of subscription offers or one-time purchases, you can add a button that will display another paywall:
```kotlin
class MyAdaptyUIPaywallsEventsObserver : AdaptyUIPaywallsEventsObserver {
override fun paywallViewDidPerformAction(view: AdaptyUIPaywallView, action: AdaptyUIAction) {
when (action) {
is AdaptyUIAction.CustomAction -> {
when (action.action) {
"login" -> {
// Handle login action - navigate to login screen
// This depends on your app's navigation system
// For example, in Compose Multiplatform:
// navController.navigate("login")
}
}
}
}
}
}
// Set up the observer
AdaptyUI.setPaywallsEventsObserver(MyAdaptyUIPaywallsEventsObserver())
```
---
# File: kmp-handling-events
---
---
title: "Kotlin Multiplatform - Handle paywall events"
description: "Handle Kotlin Multiplatform subscription events efficiently with Adapty's event tracking tools."
---
Paywalls configured with the [Paywall Builder](adapty-paywall-builder) don't need extra code to make and restore purchases. However, they generate some events that your app can respond to. Those events include button presses (close buttons, URLs, product selections, and so on) as well as notifications on purchase-related actions taken on the paywall. Learn how to respond to these events below.
:::warning
This guide is for **new Paywall Builder paywalls** only.
:::
To control or monitor processes occurring on the paywall screen within your mobile app, implement the `AdaptyUIPaywallsEventsObserver` interface methods. Some methods have default implementations that handle common scenarios automatically.
:::note
**Implementation Notes**: These methods are where you add your custom logic to respond to paywall events. You can use `view.dismiss()` to close the paywall, or implement any other custom behavior you need.
:::
## User-generated events
### Paywall appearance and disappearance
When a paywall appears or disappears, these methods will be invoked:
```kotlin showLineNumbers title="Kotlin"
override fun paywallViewDidAppear(view: AdaptyUIPaywallView) {
// Handle paywall appearance
// You can track analytics or update UI here
}
override fun paywallViewDidDisappear(view: AdaptyUIPaywallView) {
// Handle paywall disappearance
// You can track analytics or update UI here
}
```
:::note
- On iOS, `paywallViewDidAppear` is also invoked when a user taps the [web paywall button](web-paywall#step-2a-add-a-web-purchase-button) inside a paywall, and a web paywall opens in an in-app browser.
- On iOS, `paywallViewDidDisappear` is also invoked when a [web paywall](web-paywall#step-2a-add-a-web-purchase-button) opened from a paywall in an in-app browser disappears from the screen.
:::
Event examples (Click to expand)
```javascript
// Paywall appeared
{
// No additional data
}
// Paywall disappeared
{
// No additional data
}
```
### Product selection
If a user selects a product for purchase, this method will be invoked:
```kotlin showLineNumbers title="Kotlin"
override fun paywallViewDidSelectProduct(view: AdaptyUIPaywallView, productId: String) {
// Handle product selection
// You can update UI or track analytics here
}
```
Event example (Click to expand)
```javascript
{
"productId": "premium_monthly"
}
```
### Started purchase
If a user initiates the purchase process, this method will be invoked:
```kotlin showLineNumbers title="Kotlin"
override fun paywallViewDidStartPurchase(view: AdaptyUIPaywallView, product: AdaptyPaywallProduct) {
// Handle purchase start
// You can show loading indicators or track analytics here
}
```
Event example (Click to expand)
```javascript
{
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
}
}
```
### Successful, canceled, or pending purchase
If a purchase succeeds, this method will be invoked. By default, it automatically dismisses the paywall unless the purchase was canceled by the user:
```kotlin showLineNumbers title="Kotlin"
override fun paywallViewDidFinishPurchase(
view: AdaptyUIPaywallView,
product: AdaptyPaywallProduct,
purchaseResult: AdaptyPurchaseResult
) {
when (purchaseResult) {
is AdaptyPurchaseResult.Success -> {
// Check if user has access to premium features
if (purchaseResult.profile.accessLevels["premium"]?.isActive == true) {
view.dismiss()
}
}
AdaptyPurchaseResult.Pending -> {
// Handle pending purchase (e.g., user will pay offline with cash)
}
AdaptyPurchaseResult.UserCanceled -> {
// Handle user cancellation
}
}
}
```
Event examples (Click to expand)
```javascript
// Successful purchase
{
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
},
"purchaseResult": {
"type": "Success",
"profile": {
"accessLevels": {
"premium": {
"id": "premium",
"isActive": true,
"expiresAt": "2024-02-15T10:30:00Z"
}
}
}
}
}
// Pending purchase
{
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
},
"purchaseResult": {
"type": "Pending"
}
}
// User canceled purchase
{
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
},
"purchaseResult": {
"type": "UserCanceled"
}
}
```
We recommend dismissing the paywall screen in case of successful purchase.
### Failed purchase
If a purchase fails due to an error, this method will be invoked. This includes StoreKit/Google Play Billing errors (payment restrictions, invalid products, network failures), transaction verification failures, and system errors. Note that user cancellations trigger `paywallViewDidFinishPurchase` with a cancelled result instead, and pending payments do not trigger this method.
```kotlin showLineNumbers title="Kotlin"
override fun paywallViewDidFailPurchase(
view: AdaptyUIPaywallView,
product: AdaptyPaywallProduct,
error: AdaptyError
) {
// Add your purchase failure handling logic here
// For example: show error message, retry option, or custom error handling
}
```
Event example (Click to expand)
```javascript
{
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
},
"error": {
"code": "purchase_failed",
"message": "Purchase failed due to insufficient funds",
"details": {
"underlyingError": "Insufficient funds in account"
}
}
}
```
### Started restore
If a user initiates the restore process, this method will be invoked:
```kotlin showLineNumbers title="Kotlin"
override fun paywallViewDidStartRestore(view: AdaptyUIPaywallView) {
// Handle restore start
// You can show loading indicators or track analytics here
}
```
### Successful restore
If restoring a purchase succeeds, this method will be invoked:
```kotlin showLineNumbers title="Kotlin"
override fun paywallViewDidFinishRestore(view: AdaptyUIPaywallView, profile: AdaptyProfile) {
// Add your successful restore handling logic here
// For example: show success message, update UI, or dismiss paywall
// Check if user has access to premium features
if (profile.accessLevels["premium"]?.isActive == true) {
view.dismiss()
}
}
```
Event example (Click to expand)
```javascript
{
"profile": {
"accessLevels": {
"premium": {
"id": "premium",
"isActive": true,
"expiresAt": "2024-02-15T10:30:00Z"
}
},
"subscriptions": [
{
"vendorProductId": "premium_monthly",
"isActive": true,
"expiresAt": "2024-02-15T10:30:00Z"
}
]
}
}
```
We recommend dismissing the screen if the user has the required `accessLevel`. Refer to the [Subscription status](subscription-status) topic to learn how to check it.
### Failed restore
If `Adapty.restorePurchases()` fails, this method will be invoked:
```kotlin showLineNumbers title="Kotlin"
override fun paywallViewDidFailRestore(view: AdaptyUIPaywallView, error: AdaptyError) {
// Add your restore failure handling logic here
// For example: show error message, retry option, or custom error handling
}
```
Event example (Click to expand)
```javascript
{
"error": {
"code": "restore_failed",
"message": "Purchase restoration failed",
"details": {
"underlyingError": "No previous purchases found"
}
}
}
```
### Web payment navigation completion
If a user initiates the purchase process using a [web paywall](web-paywall.md), this method will be invoked:
```kotlin showLineNumbers title="Kotlin"
override fun paywallViewDidFinishWebPaymentNavigation(
view: AdaptyUIPaywallView,
product: AdaptyPaywallProduct?,
error: AdaptyError?
) {
if (error != null) {
// Handle web payment navigation error
} else {
// Handle successful web payment navigation
}
}
```
Event examples (Click to expand)
```javascript
// Successful web payment navigation
{
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
},
"error": null
}
// Failed web payment navigation
{
"product": null,
"error": {
"code": "web_payment_failed",
"message": "Web payment navigation failed",
"details": {
"underlyingError": "Network connection error"
}
}
}
```
## Data fetching and rendering
### Product loading errors
If you don't pass the products during the initialization, AdaptyUI will retrieve the necessary objects from the server by itself. If this operation fails, AdaptyUI will report the error by calling this method:
```kotlin showLineNumbers title="Kotlin"
override fun paywallViewDidFailLoadingProducts(view: AdaptyUIPaywallView, error: AdaptyError) {
// Add your product loading failure handling logic here
// For example: show error message, retry option, or custom error handling
}
```
Event example (Click to expand)
```javascript
{
"error": {
"code": "products_loading_failed",
"message": "Failed to load products from the server",
"details": {
"underlyingError": "Network timeout"
}
}
}
```
### Rendering errors
If an error occurs during the interface rendering, it will be reported by this method:
```kotlin showLineNumbers title="Kotlin"
override fun paywallViewDidFailRendering(view: AdaptyUIPaywallView, error: AdaptyError) {
// Handle rendering error
// In a normal situation, such errors should not occur
// If you come across one, please let us know
}
```
Event example (Click to expand)
```javascript
{
"error": {
"code": "rendering_failed",
"message": "Failed to render paywall interface",
"details": {
"underlyingError": "Invalid paywall configuration"
}
}
}
```
In a normal situation, such errors should not occur, so if you come across one, please let us know.
---
# File: kmp-use-fallback-paywalls
---
---
title: "Kotlin Multiplatform - Use fallback paywalls"
description: "Handle cases when users are offline or Adapty servers aren't available"
---
To maintain a fluid user experience, it is important to set up [fallbacks](/fallback-paywalls) for your [paywalls](paywalls) and [onboardings](onboardings). This precaution extends the application's capabilities in case of partial or complete loss of internet connection.
* **If the application cannot access Adapty servers:**
It will be able to display a fallback paywall, and access the local onboarding configuration.
* **If the application cannot access the internet:**
It will be able to display a fallback paywall. Onboardings include remote content and require an internet connection to function.
:::important
Before you follow the steps in this guide, [download](/local-fallback-paywalls) the fallback configuration files from Adapty.
:::
## Configuration
1. Add the fallback configuration file to your application.
* If your target platform is Android, move the fallback configuration file to the `android/app/src/main/assets/` folder.
* If your target platform is iOS, add the fallback JSON file to your project bundle. (**File** -> **Add Files to YourProjectName**)
2. Call the `.setFallback` method **before** you fetch the target paywall or onboarding.
3. Set the `assetId` parameter, depending on your target platform.
* Android: Use the file path relative to the `assets` directory.
* iOS: Use the complete filename.
```kotlin showLineNumbers
Adapty.setFallback(assetId = "fallback.json")
.onSuccess {
// Fallback paywalls loaded successfully
}
.onError { error ->
// Handle the error
}
```
Parameters:
| Parameter | Description |
| :---------- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **assetId** | Fallback configuration filename (iOS).
Fallback configuration file path, relative to the `assets` directory (Android). |
:::tip
Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality.
:::
---
# File: kmp-web-paywalls
---
---
title: "Implement web paywalls in Kotlin Multiplatform SDK"
description: "Set up a web paywall to get paid without the store fees and audits."
---
:::important
Before you begin, make sure you have [configured your web paywall in the dashboard](web-paywall.md) and installed Adapty SDK version 3.15 or later.
:::
## Open web paywalls
If you are working with a paywall you developed yourself, you need to handle web paywalls using the SDK method. The `openWebPaywall` method:
1. Generates a unique URL allowing Adapty to link a specific paywall shown to a particular user to the web page they are redirected to.
2. Tracks when your users return to the app and then requests `getProfile` at short intervals to determine whether the profile access rights have been updated.
This way, if the payment has been successful and access rights have been updated, the subscription activates in the app almost immediately.
:::note
After users return to the app, refresh the UI to reflect the profile updates. Adapty will receive and process profile update events.
:::
```kotlin showLineNumbers
viewModelScope.launch {
Adapty.openWebPaywall(product = product).onSuccess {
// the web paywall was opened successfully
}.onError { error ->
// handle the error
}
}
```
:::note
There are two versions of the `openWebPaywall` method:
1. `openWebPaywall(product = product)` that generates URLs by paywall and adds the product data to URLs as well.
2. `openWebPaywall(paywall = paywall)` that generates URLs by paywall without adding the product data to URLs. Use it when your products in the Adapty paywall differ from those in the web paywall.
:::
## Open web paywalls in an in-app browser
By default, web paywalls open in the external browser.
To provide a seamless user experience, you can open web paywalls in an in-app browser. This displays the web purchase page within your application, allowing users to complete transactions without switching apps.
To enable this, set the `openIn` parameter to `AdaptyWebPresentation.IN_APP_BROWSER`:
```kotlin showLineNumbers
viewModelScope.launch {
Adapty.openWebPaywall(
product = product,
openIn = AdaptyWebPresentation.IN_APP_BROWSER // default β EXTERNAL_BROWSER
).onSuccess {
// the web paywall was opened successfully
}.onError { error ->
// handle the error
}
}
```
---
# File: kmp-troubleshoot-paywall-builder
---
---
title: "Troubleshoot Paywall Builder in Kotlin Multiplatform SDK"
description: "Troubleshoot Paywall Builder in Kotlin Multiplatform SDK"
---
This guide helps you resolve common issues when using paywalls designed in the Adapty Paywall Builder in the Kotlin Multiplatform SDK.
## Getting a paywall configuration fails
**Issue**: The `createPaywallView` method fails to create a paywall view, or the paywall doesn't have a view configuration.
**Reason**: The paywall is not enabled for device display in the Paywall Builder.
**Solution**: Enable the **Show on device** toggle in the Paywall Builder. You can also check if a paywall has view configuration by using the `hasViewConfiguration` property on the `AdaptyPaywall` object.
## The paywall view number is too big
**Issue**: The paywall view count is showing double the expected number.
**Reason**: You may be calling `logShowPaywall` in your code, which duplicates the view count if you're using the Paywall builder. For paywalls designed with the Paywall Builder, analytics are tracked automatically, so you don't need to use this method.
**Solution**: Ensure you are not calling `logShowPaywall` in your code if you're using the Paywall builder.
---
# File: kmp-implement-paywalls-manually
---
---
title: "Implement paywalls manually in Kotlin Multiplatform SDK"
description: "Learn how to implement paywalls manually in your Kotlin Multiplatform app with Adapty SDK."
---
## Accept purchases
If you are working with paywalls you've implemented yourself, you can delegate handling purchases to Adapty, using the `makePurchase` method. This way, we will handle all the user scenarios, and you will only need to handle the purchase results.
:::important
`makePurchase` works with products created in the Adapty dashboard. Make sure you configure products and ways to retrieve them in the dashboard by following the [quickstart guide](quickstart).
:::
## Observer mode
If you want to implement your own purchase handling logic from scratch, but still want to benefit from the advanced analytics in Adapty, you can use the observer mode.
:::important
Consider the observer mode limitations [here](observer-vs-full-mode).
:::
---
# File: kmp-quickstart-manual
---
---
title: "Enable purchases in your custom paywall in Kotlin Multiplatform SDK"
description: "Integrate Adapty SDK into your custom Kotlin Multiplatform paywalls to enable in-app purchases."
---
This guide describes how to integrate Adapty into your custom paywalls. Keep full control over paywall implementation, while the Adapty SDK fetches products, handles new purchases, and restores previous ones.
:::important
**This guide is for developers who are implementing custom paywalls.** If you want the easiest way to enable purchases, use the [Adapty Paywall Builder](kmp-quickstart-paywalls.md). With Paywall Builder, you create paywalls in a no-code visual editor, Adapty handles all purchase logic automatically, and you can test different designs without republishing your app.
:::
## Before you start
### Set up products
To enable in-app purchases, you need to understand three key concepts:
- [**Products**](product.md) β anything users can buy (subscriptions, consumables, lifetime access)
- [**Paywalls**](paywalls.md) β configurations that define which products to offer. In Adapty, paywalls are the only way to retrieve products, but this design lets you modify products, prices, and offers without touching your app code.
- [**Placements**](placements.md) β where and when you show paywalls in your app (like `main`, `onboarding`, `settings`). You set up paywalls for placements in the dashboard, then request them by placement ID in your code. This makes it easy to run A/B tests and show different paywalls to different users.
Make sure you understand these concepts even if you work with your custom paywall. Basically, they are just your way to manage the products you sell in your app.
To implement your custom paywall, you will need to create a **paywall** and add it to a **placement**. This setup allows you to retrieve your products. To understand what you need to do in the dashboard, follow the quickstart guide [here](quickstart.md).
### Manage users
You can work either with or without backend authentication on your side.
However, the Adapty SDK handles anonymous and identified users differently. Read the [identification quickstart guide](kmp-quickstart-identify.md) to understand the specifics and ensure you are working with users properly.
## Step 1. Get products
To retrieve products for your custom paywall, you need to:
1. Get the `paywall` object by passing [placement](placements.md) ID to the `getPaywall` method.
2. Get the products array for this paywall using the `getPaywallProducts` method.
```kotlin showLineNumbers
fun loadPaywall() {
Adapty.getPaywall(placementId = "YOUR_PLACEMENT_ID")
.onSuccess { paywall ->
Adapty.getPaywallProducts(paywall = paywall)
.onSuccess { products ->
// Use products to build your custom paywall UI
}
.onError { error ->
// Handle the error
}
}
.onError { error ->
// Handle the error
}
}
```
## Step 2. Accept purchases
When a user taps on a product in your custom paywall, call the `makePurchase` method with the selected product. This will handle the purchase flow and return the updated profile.
```kotlin showLineNumbers
fun purchaseProduct(product: AdaptyPaywallProduct) {
Adapty.makePurchase(product = product)
.onSuccess { purchaseResult ->
when (purchaseResult) {
is AdaptyPurchaseResult.Success -> {
val profile = purchaseResult.profile
// Purchase successful, profile updated
}
is AdaptyPurchaseResult.UserCanceled -> {
// User canceled the purchase
}
is AdaptyPurchaseResult.Pending -> {
// Purchase is pending (e.g., user will pay offline with cash)
}
}
}
.onError { error ->
// Handle the error
}
}
```
## Step 3. Restore purchases
App stores require all apps with subscriptions to provide a way users can restore their purchases.
Call the `restorePurchases` method when the user taps the restore button. This will sync their purchase history with Adapty and return the updated profile.
```kotlin showLineNumbers
fun restorePurchases() {
Adapty.restorePurchases()
.onSuccess { profile ->
// Restore successful, profile updated
}
.onError { error ->
// Handle the error
}
}
```
## Next steps
Your paywall is ready to be displayed in the app. Test your purchases in the [App Store sandbox](test-purchases-in-sandbox) or in [Google Play Store](testing-on-android) to make sure you can complete a test purchase from the paywall. To see how this works in a production-ready implementation, check out the [AppViewModel.kt](https://github.com/adaptyteam/AdaptySDK-KMP/blob/main/example/composeApp/src/commonMain/kotlin/com/adapty/exampleapp/AppViewModel.kt) in our example app, which demonstrates purchase handling with proper error handling and state management.
Next, [check whether users have completed their purchase](kmp-check-subscription-status.md) to determine whether to display the paywall or grant access to paid features.
---
# File: fetch-paywalls-and-products-kmp
---
---
title: "Fetch paywalls and products for remote config paywalls in Kotlin Multiplatform SDK"
description: "Fetch paywalls and products in Adapty Kotlin Multiplatform SDK to enhance user monetization."
---
Before showcasing remote config and custom paywalls, you need to fetch the information about them. Please be aware that this topic refers to remote config and custom paywalls. For guidance on fetching paywalls for Paywall Builder-customized paywalls, please consult [Fetch Paywall Builder paywalls and their configuration](kmp-get-pb-paywalls).
:::tip
Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality.
:::
Before you start fetching paywalls and products in your mobile app (click to expand)
1. [Create your products](create-product) in the Adapty Dashboard.
2. [Create a paywall and incorporate the products into your paywall](create-paywall) in the Adapty Dashboard.
3. [Create placements and incorporate your paywall into the placement](create-placement) in the Adapty Dashboard.
4. [Install Adapty SDK](sdk-installation-kotlin-multiplatform.md) in your mobile app.
## Fetch paywall information
In Adapty, a [product](product) serves as a combination of products from both the App Store and Google Play. These cross-platform products are integrated into paywalls, enabling you to showcase them within specific mobile app placements.
To display the products, you need to obtain a [Paywall](paywalls) from one of your [placements](placements) with `getPaywall` method.
:::important
**Don't hardcode product IDs.** The only ID you should hardcode is the placement ID. Paywalls are configured remotely, so the number of products and available offers can change at any time. Your app must handle these changes dynamicallyβif a paywall returns two products today and three tomorrow, display all of them without code changes.
:::
```kotlin showLineNumbers
Adapty.getPaywall(
placementId = "YOUR_PLACEMENT_ID",
locale = "en",
fetchPolicy = AdaptyPaywallFetchPolicy.Default,
loadTimeout = 5.seconds
).onSuccess { paywall ->
// the requested paywall
}.onError { error ->
// handle the error
}
```
| Parameter | Presence | Description |
|---------|--------|-----------|
| **placementId** | required | The identifier of the [Placement](placements). This is the value you specified when creating a placement in your Adapty Dashboard. |
| **locale** | optional
default: `en`
| The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
|
| **fetchPolicy** | default: `AdaptyPaywallFetchPolicy.Default` | By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `AdaptyPaywallFetchPolicy.ReturnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores paywalls in two layers: regularly updated cache described above and [fallback paywalls](kmp-use-fallback-paywalls) . We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.
|
| **loadTimeout** | default: 5 sec | This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.
Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.
|
Don't hardcode product IDs! Since paywalls are configured remotely, the available products, the number of products, and special offers (such as free trials) can change over time. Make sure your code handles these scenarios.
For example, if you initially retrieve 2 products, your app should display those 2 products. However, if you later retrieve 3 products, your app should display all 3 without requiring any code changes. The only thing you have to hardcode is placement ID.
Response parameters:
| Parameter | Description |
| :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Paywall | An [`AdaptyPaywall`](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-paywall/) object with: a list of product IDs, the paywall identifier, remote config, and several other properties. |
## Fetch products
Once you have the paywall, you can query the product array that corresponds to it:
```kotlin showLineNumbers
Adapty.getPaywallProducts(paywall).onSuccess { products ->
// the requested products
}.onError { error ->
// handle the error
}
```
Response parameters:
| Parameter | Description |
| :-------- |:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Products | List of [`AdaptyPaywallProduct`](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-paywall-product/) objects with: product identifier, product name, price, currency, subscription length, and several other properties. |
When implementing your own paywall design, you will likely need access to these properties from the [`AdaptyPaywallProduct`](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-paywall-product/) object. Illustrated below are the most commonly used properties, but refer to the linked document for full details on all available properties.
| Property | Description |
|-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **Title** | To display the title of the product, use `product.localizedTitle`. Note that the localization is based on the users' selected store country rather than the locale of the device itself. |
| **Price** | To display a localized version of the price, use `product.price.localizedString`. This localization is based on the locale info of the device. You can also access the price as a number using `product.price.amount`. The value will be provided in the local currency. To get the associated currency symbol, use `product.price.currencySymbol`. |
| **Subscription Period** | To display the period (e.g. week, month, year, etc.), use `product.subscriptionDetails?.localizedSubscriptionPeriod`. This localization is based on the locale of the device. To fetch the subscription period programmatically, use `product.subscriptionDetails?.subscriptionPeriod`. From there you can access the `unit` enum to get the length (i.e. DAY, WEEK, MONTH, YEAR, or UNKNOWN). The `numberOfUnits` value will get you the number of period units. For example, for a quarterly subscription, you'd see `MONTH` in the unit property, and `3` in the numberOfUnits property. |
| **Introductory Offer** | To display a badge or other indicator that a subscription contains an introductory offer, check out the `product.subscriptionDetails?.introductoryOfferPhases` property. This is a list that can contain up to two discount phases: the free trial phase and the introductory price phase. Within each phase object are the following helpful properties:
β’ `paymentMode`: an enum with values `FREE_TRIAL`, `PAY_AS_YOU_GO`, `PAY_UPFRONT`, and `UNKNOWN`. Free trials will be the `FREE_TRIAL` type.
β’ `price`: The discounted price as a number. For free trials, look for `0` here.
β’ `localizedNumberOfPeriods`: a string localized using the device's locale describing the length of the offer. For example, a three day trial offer shows `3 days` in this field.
β’ `subscriptionPeriod`: Alternatively, you can get the individual details of the offer period with this property. It works in the same manner for offers as the previous section describes.
β’ `localizedSubscriptionPeriod`: A formatted subscription period of the discount for the user's locale. |
## Speed up paywall fetching with default audience paywall
Typically, paywalls are fetched almost instantly, so you don't need to worry about speeding up this process. However, in cases where you have numerous audiences and paywalls, and your users have a weak internet connection, fetching a paywall may take longer than you'd like. In such situations, you might want to display a default paywall to ensure a smooth user experience rather than showing no paywall at all.
To address this, you can use the `getPaywallForDefaultAudience` method, which fetches the paywall of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the paywall by the `getPaywall` method, as detailed in the [Fetch Paywall Information](fetch-paywalls-and-products-kmp#fetch-paywall-information) section above.
:::warning
Why we recommend using `getPaywall`
The `getPaywallForDefaultAudience` method comes with a few significant drawbacks:
- **Potential backward compatibility issues**: If you need to show different paywalls for different app versions (current and future), you may face challenges. You'll either have to design paywalls that support the current (legacy) version or accept that users with the current (legacy) version might encounter issues with non-rendered paywalls.
- **Loss of targeting**: All users will see the same paywall designed for the **All Users** audience, which means you lose personalized targeting (including based on countries, marketing attribution or your own custom attributes).
If you're willing to accept these drawbacks to benefit from faster paywall fetching, use the `getPaywallForDefaultAudience` method as follows. Otherwise, stick to the `getPaywall` described [above](fetch-paywalls-and-products-kmp#fetch-paywall-information).
:::
```kotlin showLineNumbers
Adapty.getPaywallForDefaultAudience(
placementId = "YOUR_PLACEMENT_ID",
locale = "en",
fetchPolicy = AdaptyPaywallFetchPolicy.Default
).onSuccess { paywall ->
// the requested paywall
}.onError { error ->
// handle the error
}
```
| Parameter | Presence | Description |
|---------|--------|-----------|
| **placementId** | required | The identifier of the [Placement](placements). This is the value you specified when creating a placement in your Adapty Dashboard. |
| **locale** | optional
default: `en`
| The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
|
| **fetchPolicy** | default: `AdaptyPaywallFetchPolicy.Default` | By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `AdaptyPaywallFetchPolicy.ReturnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
|
---
# File: present-remote-config-paywalls-kmp
---
---
title: "Render paywall designed by remote config in Kotlin Multiplatform SDK"
description: "Discover how to present remote config paywalls in Adapty Kotlin Multiplatform SDK to personalize user experience."
---
If you've customized a paywall using remote config, you'll need to implement rendering in your mobile app's code to display it to users. Since remote config offers flexibility tailored to your needs, you're in control of what's included and how your paywall view appears. We provide a method for fetching the remote configuration, giving you the autonomy to showcase your custom paywall configured via remote config.
## Get paywall remote config and present it
To get a remote config of a paywall, access the `remoteConfig` property and extract the needed values.
```kotlin showLineNumbers
Adapty.getPaywall(
placementId = "YOUR_PLACEMENT_ID",
locale = "en",
fetchPolicy = AdaptyPaywallFetchPolicy.Default,
loadTimeout = 5.seconds
).onSuccess { paywall ->
val headerText = paywall.remoteConfig?.dataMap?.get("header_text") as? String
// use the remote config values
}.onError { error ->
// handle the error
}
```
At this point, once you've received all the necessary values, it's time to render and assemble them into a visually appealing page. Ensure that the design accommodates various mobile phone screens and orientations, providing a seamless and user-friendly experience across different devices.
:::warning
Make sure to [record the paywall view event](present-remote-config-paywalls-kmp#track-paywall-view-events) as described below, allowing Adapty analytics to capture information for funnels and A/B tests.
:::
After you've done with displaying the paywall, continue with setting up a purchase flow. When the user makes a purchase, simply call `.makePurchase()` with the product from your paywall. For details on the`.makePurchase()` method, read [Making purchases](kmp-making-purchases).
We recommend [creating a backup paywall called a fallback paywall](kmp-use-fallback-paywalls). This backup will display to the user when there's no internet connection or cache available, ensuring a smooth experience even in these situations.
## Track paywall view events
Adapty assists you in measuring the performance of your paywalls. While we gather data on purchases automatically, logging paywall views needs your input because only you know when a customer sees a paywall.
To log a paywall view event, simply call `.logShowPaywall(paywall)`, and it will be reflected in your paywall metrics in funnels and A/B tests.
:::important
Calling `.logShowPaywall(paywall)` is not needed if you are displaying paywalls created in the [paywall builder](adapty-paywall-builder.md).
:::
```kotlin showLineNumbers
Adapty.logShowPaywall(paywall = paywall)
.onSuccess {
// paywall view logged successfully
}
.onError { error ->
// handle the error
}
```
Request parameters:
| Parameter | Presence | Description |
| :---------- | :------- |:-------------------------------------------------------------------------------------------------------|
| **paywall** | required | An [`AdaptyPaywall`](https://kmp.adapty.io//////adapty/com.adapty.kmp.models/-adapty-paywall/) object. |
---
# File: kmp-making-purchases
---
---
title: "Make purchases in mobile app in Kotlin Multiplatform SDK"
description: "Guide on handling in-app purchases and subscriptions using Adapty."
---
Displaying paywalls within your mobile app is an essential step in offering users access to premium content or services. However, simply presenting these paywalls is enough to support purchases only if you use [Paywall Builder](adapty-paywall-builder) to customize your paywalls.
If you don't use the Paywall Builder, you must use a separate method called `.makePurchase()` to complete a purchase and unlock the desired content. This method serves as the gateway for users to engage with the paywalls and proceed with their desired transactions.
If your paywall has an active promotional offer for the product a user is trying to buy, Adapty will automatically apply it at the time of purchase.
:::warning
Keep in mind that the introductory offer will be applied automatically only if you use the paywalls set up using the Paywall Builder.
In other cases, you'll need to [verify the user's eligibility for an introductory offer on iOS](fetch-paywalls-and-products-kmp#check-intro-offer-eligibility-on-ios). Skipping this step may result in your app being rejected during release. Moreover, it could lead to charging the full price to users who are eligible for an introductory offer.
:::
Make sure you've [done the initial configuration](quickstart) without skipping a single step. Without it, we can't validate purchases.
## Make purchase
:::note
**Using [Paywall Builder](adapty-paywall-builder)?** Purchases are processed automaticallyβyou can skip this step.
**Looking for step-by-step guidance?** Check out the [quickstart guide](kmp-implement-paywalls-manually) for end-to-end implementation instructions with full context.
:::
```kotlin showLineNumbers
Adapty.makePurchase(product = product).onSuccess { purchaseResult ->
when (purchaseResult) {
is AdaptyPurchaseResult.Success -> {
val profile = purchaseResult.profile
if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive == true) {
// Grant access to the paid features
}
}
is AdaptyPurchaseResult.UserCanceled -> {
// Handle the case where the user canceled the purchase
}
is AdaptyPurchaseResult.Pending -> {
// Handle deferred purchases (e.g., the user will pay offline with cash)
}
}
}.onError { error ->
// Handle the error
}
```
Request parameters:
| Parameter | Presence | Description |
| :---------- | :------- |:----------------------------------------------------------------------------------------------------------------------------------------------|
| **Product** | required | An [`AdaptyPaywallProduct`](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-paywall-product/) object retrieved from the paywall. |
Response parameters:
| Parameter | Description |
|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **Profile** | If the request has been successful, the response contains this object. An [AdaptyProfile](khttps://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-profile/) object provides comprehensive information about a user's access levels, subscriptions, and non-subscription purchases within the app.
Check the access level status to ascertain whether the user has the required access to the app.
|
:::warning
**Note:** if you're still on Apple's StoreKit version lower than v2.0 and Adapty SDK version lowers than v.2.9.0, you need to provide [Apple App Store shared secret](app-store-connection-configuration#step-4-enter-app-store-shared-secret) instead. This method is currently deprecated by Apple.
:::
## Change subscription when making a purchase
When a user opts for a new subscription instead of renewing the current one, the way it works depends on the app store. For Google Play, the subscription isn't automatically updated. You'll need to manage the switch in your mobile app code as described below.
To replace the subscription with another one in Android, call `.makePurchase()` method with the additional parameter:
```kotlin showLineNumbers
val subscriptionUpdateParams = AdaptyAndroidSubscriptionUpdateParameters(
oldSubVendorProductId = "old_subscription_product_id",
replacementMode = AdaptyAndroidSubscriptionUpdateReplacementMode.CHARGE_FULL_PRICE
)
val purchaseParams = AdaptyPurchaseParameters.Builder()
.setSubscriptionUpdateParams(subscriptionUpdateParams)
.build()
Adapty.makePurchase(
product = product,
parameters = purchaseParams
).onSuccess { purchaseResult ->
when (purchaseResult) {
is AdaptyPurchaseResult.Success -> {
val profile = purchaseResult.profile
// successful cross-grade
}
is AdaptyPurchaseResult.UserCanceled -> {
// user canceled the purchase flow
}
is AdaptyPurchaseResult.Pending -> {
// the purchase has not been finished yet, e.g. user will pay offline by cash
}
}
}.onError { error ->
// Handle the error
}
```
Additional request parameter:
| Parameter | Presence | Description |
|:---------------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **parameters** | optional | an [`AdaptyAndroidSubscriptionUpdateParameters`](https://kmp.adapty.io/////adapty/com.adapty.kmp.models/-adapty-android-subscription-update-parameters/) object passed through [`AdaptyPurchaseParameters`](https://kmp.adapty.io/adapty/com.adapty.kmp.models/-adapty-purchase-parameters/). |
You can read more about subscriptions and replacement modes in the Google Developer documentation:
- [About replacement modes](https://developer.android.com/google/play/billing/subscriptions#replacement-modes)
- [Recommendations from Google for replacement modes](https://developer.android.com/google/play/billing/subscriptions#replacement-recommendations)
- Replacement mode [`CHARGE_PRORATED_PRICE`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#CHARGE_PRORATED_PRICE()). Note: this method is available only for subscription upgrades. Downgrades are not supported.
- Replacement mode [`DEFERRED`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#DEFERRED()). Note: A real subscription change will occur only when the current subscription billing period ends.
## Redeem Offer Code in iOS
Since iOS 14.0, your users can redeem Offer Codes. Code redemption means using a special code, like a promotional or gift card code, to get free access to content or features in an app or on the App Store. To enable users to redeem offer codes, you can display the offer code redemption sheet by using the appropriate SDK method:
```kotlin showLineNumbers
Adapty.presentCodeRedemptionSheet()
.onSuccess {
// code redemption sheet presented successfully
}
.onError { error ->
// handle the error
}
```
:::danger
Based on our observations, the Offer Code Redemption sheet in some apps may not work reliably. We recommend redirecting the user directly to the App Store.
In order to do this, you need to open the url of the following format:
`https://apps.apple.com/redeem?ctx=offercodes&id={apple_app_id}&code={code}`
:::
## Manage prepaid plans (Android)
If your app users can purchase [prepaid plans](https://developer.android.com/google/play/billing/subscriptions#prepaid-plans) (e.g., buy a non-renewable subscription for several months), you can enable [pending transactions](https://developer.android.com/google/play/billing/subscriptions#pending) for prepaid plans.
```kotlin showLineNumbers
Adapty.activate(
AdaptyConfig.Builder("PUBLIC_SDK_KEY")
.withGoogleEnablePendingPrepaidPlans(true)
.build()
).onSuccess {
// successful activation
}.onError { error ->
// handle the error
}
```
---
# File: kmp-restore-purchase
---
---
title: "Restore purchases in mobile app in Kotlin Multiplatform SDK"
description: "Learn how to restore purchases in Adapty to ensure seamless user experience."
---
Restoring Purchases is a feature that allows users to regain access to previously purchased content, such as subscriptions or in-app purchases, without being charged again. This feature is especially useful for users who may have uninstalled and reinstalled the app or switched to a new device and want to access their previously purchased content without paying again.
:::note
In paywalls built with [Paywall Builder](adapty-paywall-builder), purchases are restored automatically without additional code from you. If that's your case β you can skip this step.
:::
To restore a purchase if you do not use the [Paywall Builder](adapty-paywall-builder) to customize the paywall, call `.restorePurchases()` method:
```kotlin showLineNumbers
Adapty.restorePurchases().onSuccess { profile ->
if (profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive == true) {
// successful access restore
}
}.onError { error ->
// handle the error
}
```
Response parameters:
| Parameter | Description |
|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **Profile** | An [`AdaptyProfile`](https://kmp.adapty.io//////adapty/com.adapty.kmp.models/-adapty-profile/) object. This model contains info about access levels, subscriptions, and non-subscription purchases.
Π‘heck the **access level status** to determine whether the user has access to the app.
|
:::tip
Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality.
:::
---
# File: implement-observer-mode-kmp
---
---
title: "Implement Observer mode in Kotlin Multiplatform SDK"
description: "Implement observer mode in Adapty to track user subscription events in Kotlin Multiplatform SDK."
---
If you already have your own purchase infrastructure and aren't ready to fully switch to Adapty, you can explore [Observer mode](observer-vs-full-mode). In its basic form, Observer Mode offers advanced analytics and seamless integration with attribution and analytics systems.
If this meets your needs, you only need to:
1. Turn it on when configuring the Adapty SDK by setting the `observerMode` parameter to `true`. Follow the setup instructions for [Kotlin Multiplatform](sdk-installation-kotlin-multiplatform.md).
2. [Report transactions](report-transactions-observer-mode-kmp) from your existing purchase infrastructure to Adapty.
## Observer mode setup
Turn on the Observer mode if you handle purchases and subscription status yourself and use Adapty for sending subscription events and analytics.
:::important
When running in Observer mode, Adapty SDK won't close any transactions, so make sure you're handling it.
:::
```kotlin showLineNumbers
val config = AdaptyConfig
.Builder("PUBLIC_SDK_KEY")
.withObserverMode(true) // default false
.build()
Adapty.activate(configuration = config)
.onSuccess {
Log.d("Adapty", "SDK initialised in observer mode")
}
.onError { error ->
Log.e("Adapty", "Adapty init error: ${error.message}")
}
```
Parameters:
| Parameter | Description |
| --------------------------- | ------------------------------------------------------------ |
| observerMode | A boolean value that controls [Observer mode](observer-vs-full-mode). The default value is `false`. |
## Using Adapty paywalls in Observer Mode
If you also want to use Adapty's paywalls and A/B testing features, you can β but it requires some extra setup in Observer mode. Here's what you'll need to do in addition to the steps above:
1. Display paywalls as usual for [remote config paywalls](present-remote-config-paywalls-kmp.md).
3. [Associate paywalls](report-transactions-observer-mode-kmp) with purchase transactions.
---
# File: report-transactions-observer-mode-kmp
---
---
title: "Report transactions in Observer Mode in Kotlin Multiplatform SDK"
description: "Report purchase transactions in Adapty Observer Mode for user insights and revenue tracking in Kotlin Multiplatform SDK."
---
In Observer mode, the Adapty SDK can't track purchases made through your existing purchase system on its own. You need to report transactions from your app store. It's crucial to set this up **before** releasing your app to avoid errors in analytics.
Use `reportTransaction` to explicitly report each transaction for Adapty to recognize it.
:::warning
**Don't skip transaction reporting!**
If you don't call `reportTransaction`, Adapty won't recognize the transaction, it won't appear in analytics, and it won't be sent to integrations.
:::
If you use Adapty paywalls, include the `variationId` when reporting a transaction. This links the purchase to the paywall that triggered it, ensuring accurate paywall analytics.
```kotlin showLineNumbers
Adapty.reportTransaction(
transactionId = "your_transaction_id",
variationId = paywall.variationId
).onSuccess { profile ->
// Transaction reported successfully
// profile contains updated user data
}.onError { error ->
// handle the error
}
```
Parameters:
| Parameter | Presence | Description |
| --------------- | -------- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| transactionId | required | The transaction ID from your app store purchase. This is typically the purchase token or transaction identifier returned by the store. |
| variationId | optional | The string identifier of the variation. You can get it using `variationId` property of the [AdaptyPaywall](https://kmp.adapty.io//////adapty/com.adapty.kmp.models/-adapty-paywall/) object. |
---
# File: kmp-troubleshoot-purchases
---
---
title: "Troubleshoot purchases in Kotlin Multiplatform SDK"
description: "Troubleshoot purchases in Kotlin Multiplatform SDK"
---
This guide helps you resolve common issues when implementing purchases manually in the Kotlin Multiplatform SDK.
## makePurchase is called successfully, but the profile is not being updated
**Issue**: The `makePurchase` method completes successfully, but the user's profile and subscription status are not updated in Adapty.
**Reason**: This usually indicates incomplete Google Play Store setup or configuration issues.
**Solution**: Ensure you've completed all the [Google Play setup steps](https://adapty.io/docs/initial-android).
## makePurchase is invoked twice
**Issue**: The `makePurchase` method is being called multiple times for the same purchase.
**Reason**: This typically happens when the purchase flow is triggered multiple times due to UI state management issues or rapid user interactions.
**Solution**: Ensure you've completed all the [Google Play setup steps](https://adapty.io/docs/initial-android).
## AdaptyError.cantMakePayments in observer mode
**Issue**: You're getting `AdaptyError.cantMakePayments` when using `makePurchase` in observer mode.
**Reason**: In observer mode, you should handle purchases on your side, not use Adapty's `makePurchase` method.
**Solution**: If you use `makePurchase` for purchases, turn off the observer mode. You need either to use `makePurchase` or handle purchases on your side in the observer mode. See [Implement Observer mode](implement-observer-mode-kmp) for more details.
## Adapty error: (code: 103, message: Play Market request failed on purchases updated: responseCode=3, debugMessage=Billing Unavailable, detail: null)
**Issue**: You're receiving a billing unavailable error from Google Play Store.
**Reason**: This error is not related to Adapty. It's a Google Play Billing Library error indicating that billing is not available on the device.
**Solution**: This error is not related to Adapty. You can check and find out more about it in the Play Store documentation: [Handle BillingResult response codes](https://developer.android.com/google/play/billing/errors#billing_unavailable_error_code_3) | Play Billing | Android Developers.
## Not found makePurchasesCompletionHandlers
**Issue**: You're encountering issues with `makePurchasesCompletionHandlers` not being found.
**Reason**: This is typically related to sandbox testing issues.
**Solution**: Create a new sandbox user and try again. This often resolves sandbox-related purchase completion handler issues.
---
# File: kmp-user
---
---
title: "Users & access in Kotlin Multiplatform SDK"
description: "Learn how to work with users and access levels in your Kotlin Multiplatform app with Adapty SDK."
---
This page contains all guides for working with users and access levels in your Kotlin Multiplatform app. Choose the topic you need:
- **[Identify users](kmp-identifying-users)** - Learn how to identify users in your app
- **[Update user data](kmp-setting-user-attributes)** - Set user attributes and profile data
- **[Listen for subscription status changes](kmp-listen-subscription-changes)** - Monitor subscription changes in real-time
- **[Kids Mode](kids-mode-kmp)** - Implement Kids Mode for your app
---
# File: kmp-identifying-users
---
---
title: "Identify users in Kotlin Multiplatform SDK"
description: "Identify users in Adapty to improve personalized subscription experiences."
---
Adapty creates an internal profile ID for every user. However, if you have your own authentication system, you should set your own Customer User ID. You can find users by their Customer User ID in the [Profiles](profiles-crm) section and use it in the [server-side API](getting-started-with-server-side-api), which will be sent to all integrations.
### Setting customer user ID on configuration
If you have a user ID during configuration, just pass it as `customerUserId` parameter to `.activate()` method:
```kotlin showLineNumbers
Adapty.activate(
AdaptyConfig.Builder("PUBLIC_SDK_KEY")
.withCustomerUserId("YOUR_USER_ID")
.build()
).onSuccess {
// successful activation
}.onError { error ->
// handle the error
}
}
```
:::tip
Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality.
:::
### Setting customer user ID after configuration
If you don't have a user ID in the SDK configuration, you can set it later at any time with the `.identify()` method. The most common cases for using this method are after registration or authorization, when the user switches from being an anonymous user to an authenticated user.
```kotlin showLineNumbers
Adapty.identify("YOUR_USER_ID").onSuccess {
// successful identify
}.onError { error ->
// handle the error
}
```
Request parameters:
- **Customer User ID** (required): a string user identifier.
:::warning
Resubmitting of significant user data
In some cases, such as when a user logs into their account again, Adapty's servers already have information about that user. In these scenarios, the Adapty SDK will automatically switch to work with the new user. If you passed any data to the anonymous user, such as custom attributes or attributions from third-party networks, you should resubmit that data for the identified user.
It's also important to note that you should re-request all paywalls and products after identifying the user, as the new user's data may be different.
:::
### Logging out and logging in
You can log the user out anytime by calling `.logout()` method:
```kotlin showLineNumbers
Adapty.logout().onSuccess {
// successful logout
}.onError { error ->
// handle the error
}
```
You can then login the user using `.identify()` method.
## Assign `appAccountToken` (iOS)
[`iosAppAccountToken`](https://developer.apple.com/documentation/storekit/product/purchaseoption/appaccounttoken(_:)) is a **UUID** that lets you link App Store transactions to your internal user identity.
StoreKit associates this token with every transaction, so your backend can match App Store data to your users.
Use a stable UUID generated per user and reuse it for the same account across devices.
This ensures that purchases and App Store notifications stay correctly linked.
You can set the token in two ways β during the SDK activation or when identifying the user.
:::important
You must always pass `iosAppAccountToken` together with `customerUserId`.
If you pass only the token, it will not be included in the transaction.
:::
```kotlin showLineNumbers
// During configuration:
Adapty.activate(
AdaptyConfig.Builder("PUBLIC_SDK_KEY")
.withCustomerUserId(
id = "YOUR_USER_ID",
iosAppAccountToken = "YOUR_IOS_APP_ACCOUNT_TOKEN"
)
.build()
).onSuccess {
// successful activation
}.onError { error ->
// handle the error
}
// Or when identifying users
Adapty.identify(
customerUserId = "YOUR_USER_ID",
iosAppAccountToken = "YOUR_IOS_APP_ACCOUNT_TOKEN"
).onSuccess {
// successful identify
}.onError { error ->
// handle the error
}
```
## Set obfuscated account IDs (Android)
Google Play requires obfuscated account IDs for certain use cases to enhance user privacy and security. These IDs help Google Play identify purchases while keeping user information anonymous, which is particularly important for fraud prevention and analytics.
You may need to set these IDs if your app handles sensitive user data or if you're required to comply with specific privacy regulations. The obfuscated IDs allow Google Play to track purchases without exposing actual user identifiers.
:::important
You must always pass `androidObfuscatedAccountId` together with `customerUserId`.
If you pass only the obfuscated account ID, it will not be included in the transaction.
:::
```kotlin showLineNumbers
// During configuration:
Adapty.activate(
AdaptyConfig.Builder("PUBLIC_SDK_KEY")
.withCustomerUserId(
id = "YOUR_USER_ID",
androidObfuscatedAccountId = "YOUR_OBFUSCATED_ACCOUNT_ID"
)
.build()
).onSuccess {
// successful activation
}.onError { error ->
// handle the error
}
// Or when identifying users
Adapty.identify(
customerUserId = "YOUR_USER_ID",
androidObfuscatedAccountId = "YOUR_OBFUSCATED_ACCOUNT_ID"
).onSuccess {
// successful identify
}.onError { error ->
// handle the error
}
```
---
# File: kmp-setting-user-attributes
---
---
title: "Set user attributes in Kotlin Multiplatform SDK"
description: "Learn how to set user attributes in Adapty to enable better audience segmentation."
---
You can set optional attributes such as email, phone number, etc, to the user of your app. You can then use attributes to create user [segments](segments) or just view them in CRM.
### Setting user attributes
To set user attributes, call `.updateProfile()` method:
```kotlin showLineNumbers
val builder = AdaptyProfileParameters.Builder()
.withEmail("email@email.com")
.withPhoneNumber("+18888888888")
.withFirstName("John")
.withLastName("Appleseed")
.withGender(AdaptyProfile.Gender.FEMALE)
.withBirthday(AdaptyProfile.Date(1970, 1, 3))
Adapty.updateProfile(builder.build())
.onSuccess {
// profile updated successfully
}
.onError { error ->
// handle the error
}
```
Please note that the attributes that you've previously set with the `updateProfile` method won't be reset.
:::tip
Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality.
:::
### The allowed keys list
The allowed keys `` of `AdaptyProfileParameters.Builder` and the values `` are listed below:
| Key | Value |
|---|-----|
| email
phoneNumber
firstName
lastName
| String |
| gender | Enum, allowed values are: `AdaptyProfile.Gender.FEMALE`, `AdaptyProfile.Gender.MALE`, `AdaptyProfile.Gender.OTHER` |
| birthday | Date |
### Custom user attributes
You can set your own custom attributes. These are usually related to your app usage. For example, for fitness applications, they might be the number of exercises per week, for language learning app user's knowledge level, and so on. You can use them in segments to create targeted paywalls and offers, and you can also use them in analytics to figure out which product metrics affect the revenue most.
```kotlin showLineNumbers
val builder = AdaptyProfileParameters.Builder()
builder.withCustomAttribute("key1", "value1")
```
To remove existing key, use `.withRemovedCustomAttribute()` method:
```kotlin showLineNumbers
val builder = AdaptyProfileParameters.Builder()
builder.withRemovedCustomAttribute("key2")
```
Sometimes you need to figure out what custom attributes have already been installed before. To do this, use the `customAttributes` field of the `AdaptyProfile` object.
:::warning
Keep in mind that the value of `customAttributes` may be out of date since the user attributes can be sent from different devices at any time so the attributes on the server might have been changed after the last sync.
:::
### Limits
- Up to 30 custom attributes per user
- Key names are up to 30 characters long. The key name can include alphanumeric characters and any of the following: `_` `-` `.`
- Value can be a string or float with no more than 50 characters.
---
# File: kmp-listen-subscription-changes
---
---
title: "Check subscription status in Kotlin Multiplatform SDK"
description: "Track and manage user subscription status in Adapty for improved customer retention in your Kotlin Multiplatform app."
---
With Adapty, keeping track of subscription status is made easy. You don't have to manually insert product IDs into your code. Instead, you can effortlessly confirm a user's subscription status by checking for an active [access level](access-level).
Before you start checking subscription status, set up [Real-time Developer Notifications (RTDN)](enable-real-time-developer-notifications-rtdn).
## Access level and the AdaptyProfile object
Access levels are properties of the [AdaptyProfile](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-profile/) object. We recommend retrieving the profile when your app starts, such as when you [identify a user](android-identifying-users#setting-customer-user-id-on-configuration) , and then updating it whenever changes occur. This way, you can use the profile object without repeatedly requesting it.
To be notified of profile updates, listen for profile changes as described in the [Listening for profile updates, including access levels](android-listen-subscription-changes.md) section below.
:::tip
Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality.
:::
## Retrieving the access level from the server
To get the access level from the server, use the `.getProfile()` method:
```kotlin showLineNumbers
Adapty.getProfile().onSuccess { profile ->
// check the access
}.onError { error ->
// handle the error
}
```
Response parameters:
| Parameter | Description |
| --------- |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Profile | An [AdaptyProfile](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-profile/) object. Generally, you have to check only the access level status of the profile to determine whether the user has premium access to the app.
The `.getProfile` method provides the most up-to-date result as it always tries to query the API. If for some reason (e.g. no internet connection), the Adapty SDK fails to retrieve information from the server, the data from the cache will be returned. It is also important to note that the Adapty SDK updates `AdaptyProfile` cache regularly, to keep this information as up-to-date as possible.
|
The `.getProfile()` method provides you with the user profile from which you can get the access level status. You can have multiple access levels per app. For example, if you have a newspaper app and sell subscriptions to different topics independently, you can create access levels "sports" and "science". But most of the time, you will only need one access level, in that case, you can just use the default "premium" access level.
Here is an example for checking for the default "premium" access level:
```kotlin showLineNumbers
Adapty.getProfile().onSuccess { profile ->
if (profile.accessLevels["premium"]?.isActive == true) {
// grant access to premium features
}
}.onError { error ->
// handle the error
}
```
### Listening for subscription status updates
Whenever the user's subscription changes, Adapty fires an event.
To receive messages from Adapty, you need to make some additional configuration:
```kotlin showLineNumbers
Adapty.setOnProfileUpdatedListener { profile ->
// handle any changes to subscription state
}
```
Adapty also fires an event at the start of the application. In this case, the cached subscription status will be passed.
### Subscription status cache
The cache implemented in the Adapty SDK stores the subscription status of the profile. This means that even if the server is unavailable, the cached data can be accessed to provide information about the profile's subscription status.
However, it's important to note that direct data requests from the cache are not possible. The SDK periodically queries the server every minute to check for any updates or changes related to the profile. If there are any modifications, such as new transactions or other updates, they will be sent to the cached data in order to keep it synchronized with the server.
---
# File: kmp-deal-with-att
---
---
title: "Deal with ATT in Kotlin Multiplatform SDK"
description: "Get started with Adapty on Kotlin Multiplatform to streamline subscription setup and management."
---
If your application uses AppTrackingTransparency framework and presents an app-tracking authorization request to the user, then you should send the [authorization status](https://developer.apple.com/documentation/apptrackingtransparency/attrackingmanager/authorizationstatus/) to Adapty.
```kotlin showLineNumbers
val profileParameters = AdaptyProfileParameters.Builder()
.withAttStatus(3) // 3 = ATTrackingManagerAuthorizationStatusAuthorized
.build()
Adapty.updateProfile(profileParameters)
.onSuccess {
// ATT status updated successfully
}
.onError { error ->
// handle AdaptyError
}
```
:::warning
We strongly recommend that you send this value as early as possible when it changes, only in that case the data will be sent in a timely manner to the integrations you have configured.
:::
---
# File: kids-mode-kmp
---
---
title: "Kids Mode in Kotlin Multiplatform SDK"
description: "Easily enable Kids Mode to comply with Google policies. No GAID or ad data collected in Kotlin Multiplatform SDK."
---
If your Kotlin Multiplatform application is intended for kids, you must follow the policies of [Google](https://support.google.com/googleplay/android-developer/answer/9893335). If you're using the Adapty SDK, a few simple steps will help you configure it to meet these policies and pass app store reviews.
## What's required?
You need to configure the Adapty SDK to disable the collection of:
- [IDFA (Identifier for Advertisers)](https://en.wikipedia.org/wiki/Identifier_for_Advertisers) (iOS)
- [Android Advertising ID (AAID/GAID)](https://support.google.com/googleplay/android-developer/answer/6048248) (Android)
- [IP address](https://www.ftc.gov/system/files/ftc_gov/pdf/p235402_coppa_application.pdf)
In addition, we recommend using customer user ID carefully. User ID in format `` will be definitely treated as gathering personal data as well as using email. For Kids Mode, a best practice is to use randomized or anonymized identifiers (e.g., hashed IDs or device-generated UUIDs) to ensure compliance.
## Enabling Kids Mode
### Updates in the Adapty Dashboard
In the Adapty Dashboard, you need to disable the IP address collection. To do this, go to [App settings](https://app.adapty.io/settings/general) and click **Disable IP address collection** under **Collect users' IP address**.
### Updates in your mobile app code
To comply with policies, you need to disable the collection of the Android Advertising ID (AAID/GAID) and IP address when initializing the Adapty SDK:
```kotlin showLineNumbers
override fun onCreate() {
super.onCreate()
val config = AdaptyConfig
.Builder("PUBLIC_SDK_KEY")
// highlight-start
.withGoogleAdvertisingIdCollectionDisabled(true) // set to `true`
.withIpAddressCollectionDisabled(true) // set to `true`
// highlight-end
.build()
Adapty.activate(configuration = config)
.onSuccess {
Log.d("Adapty", "SDK initialised with privacy settings")
}
.onError { error ->
Log.e("Adapty", "Adapty init error: ${error.message}")
}
}
```
---
# File: kmp-onboardings
---
---
title: "Onboardings in Kotlin Multiplatform SDK"
description: "Learn how to work with onboardings in your Kotlin Multiplatform app with Adapty SDK."
---
---
# File: kmp-get-onboardings
---
---
title: "Get onboardings in Kotlin Multiplatform SDK"
description: "Learn how to retrieve onboardings in Adapty for Kotlin Multiplatform."
---
After [you designed the visual part for your onboarding](design-onboarding.md) with the builder in the Adapty Dashboard, you can display it in your Kotlin Multiplatform app. The first step in this process is to get the onboarding associated with the placement and its view configuration as described below.
Before you start, ensure that:
1. You have installed [Adapty Kotlin Multiplatform SDK](sdk-installation-kotlin-multiplatform.md) version 3.15.0 or higher.
2. You have [created an onboarding](create-onboarding.md).
3. You have added the onboarding to a [placement](placements.md).
## Fetch onboarding
When you create an [onboarding](onboardings.md) with our no-code builder, it's stored as a container with configuration that your app needs to fetch and display. This container manages the entire experience - what content appears, how it's presented, and how user interactions (like quiz answers or form inputs) are processed. The container also automatically tracks analytics events, so you don't need to implement separate view tracking.
For the best performance, fetch the onboarding configuration early to give images enough time to download before showing to users.
To get an onboarding, use the `getOnboarding` method:
```kotlin showLineNumbers
Adapty.getOnboarding(
placementId = "YOUR_PLACEMENT_ID",
locale = "en",
fetchPolicy = AdaptyPaywallFetchPolicy.Default,
loadTimeout = 5.seconds
).onSuccess { paywall ->
// the requested paywall
}.onError { error ->
// handle the error
}
```
Parameters:
| Parameter | Presence | Description |
|---------|--------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. |
| **locale** | optional
default: `en`
| The identifier of the onboarding localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
|
| **fetchPolicy** | default: `.reloadRevalidatingCacheData` | By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores onboardings locally in two layers: regularly updated cache described above and fallback onboardings. We also use CDN to fetch onboardings faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your onboardings while ensuring reliability even in cases where internet connection is scarce.
|
| **loadTimeout** | default: 5 sec | This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.
Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.
|
Response parameters:
| Parameter | Description |
|:----------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Onboarding | An [`AdaptyOnboarding`](https://kmp.adapty.io///adapty/com.adapty.kmp.models/-adapty-onboarding/) object with: the onboarding identifier and configuration, remote config, and several other properties. |
## Speed up onboarding fetching with default audience onboarding
Typically, onboardings are fetched almost instantly, so you don't need to worry about speeding up this process. However, in cases where you have numerous audiences and onboardings, and your users have a weak internet connection, fetching a onboarding may take longer than you'd like. In such situations, you might want to display a default onboarding to ensure a smooth user experience rather than showing no onboarding at all.
To address this, you can use the `getOnboardingForDefaultAudience` method, which fetches the onboarding of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the onboarding by the `getOnboarding` method, as detailed in the [Fetch Onboarding](#fetch-onboarding) section above.
:::warning
Consider using `getOnboarding` instead of `getOnboardingForDefaultAudience`, as the latter has important limitations:
- **Compatibility issues**: May create problems when supporting multiple app versions, requiring either backward-compatible designs or accepting that older versions might display incorrectly.
- **No personalization**: Only shows content for the "All Users" audience, removing targeting based on country, attribution, or custom attributes.
If faster fetching outweighs these drawbacks for your use case, use `getOnboardingForDefaultAudience` as shown below. Otherwise, use `getOnboarding` as described [above](#fetch-onboarding).
:::
```kotlin showLineNumbers
Adapty.getOnboardingForDefaultAudience(
placementId = "YOUR_PLACEMENT_ID",
locale = "en",
fetchPolicy = AdaptyPaywallFetchPolicy.Default,
).onSuccess { paywall ->
// the requested paywall
}.onError { error ->
// handle the error
}
```
Parameters:
| Parameter | Presence | Description |
|---------|--------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. |
| **locale** | optional
default: `en`
| The identifier of the onboarding localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language. |
| **fetchPolicy** | default: `.reloadRevalidatingCacheData` | By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores onboardings locally in two layers: regularly updated cache described above and fallback onboardings. We also use CDN to fetch onboardings faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your onboardings while ensuring reliability even in cases where internet connection is scarce.
|
---
# File: kmp-present-onboardings
---
---
title: "Present onboardings in Kotlin Multiplatform SDK"
description: "Learn how to present onboardings effectively to drive more conversions."
---
If you've customized an onboarding using the builder, you don't need to worry about rendering it in your Kotlin Multiplatform app code to display it to the user. Such an onboarding contains both what should be shown within the onboarding and how it should be shown.
Before you start, ensure that:
1. You have installed [Adapty Kotlin Multiplatform SDK](sdk-installation-kotlin-multiplatform.md) 3.15.0 or later.
2. You have [created an onboarding](create-onboarding.md).
3. You have added the onboarding to a [placement](placements.md).
To display an onboarding, use the `view.present()` method on the `view` created by the `createOnboardingView` method. Each `view` can only be used once. If you need to display the onboarding again, call `createPaywallView` one more to create a new `view` instance.
:::warning
Reusing the same `view` without recreating it may result in an error.
:::
```kotlin showLineNumbers title="Kotlin Multiplatform"
viewModelScope.launch {
AdaptyUI.createOnboardingView(onboarding = onboarding).onSuccess { view ->
view.present()
}.onError { error ->
// handle the error
}
}
```
## Configure iOS presentation style
Configure how the onboarding is presented on iOS by passing the `iosPresentationStyle` parameter to the `present()` method. The parameter accepts `AdaptyUIIOSPresentationStyle.FULLSCREEN` (default) or `AdaptyUIIOSPresentationStyle.PAGESHEET` values.
```kotlin showLineNumbers
viewModelScope.launch {
val view = AdaptyUI.createOnboardingView(onboarding = onboarding).getOrNull()
view?.present(iosPresentationStyle = AdaptyUIIOSPresentationStyle.PAGESHEET)
}
```
## Customize how links open in onboardings
By default, links in onboardings open in an in-app browser. This provides a seamless user experience by displaying web pages within your application, allowing users to view them without switching apps.
If you prefer to open links in an external browser instead, you can customize this behavior by setting the `externalUrlsPresentation` parameter to `AdaptyWebPresentation.EXTERNAL_BROWSER`:
```kotlin showLineNumbers
viewModelScope.launch {
AdaptyUI.createOnboardingView(
onboarding = onboarding,
externalUrlsPresentation = AdaptyWebPresentation.EXTERNAL_BROWSER // default β IN_APP_BROWSER
).onSuccess { view ->
view.present()
}.onError { error ->
// handle the error
}
}
```
---
# File: kmp-handling-onboarding-events
---
---
title: "Handle onboarding events in Kotlin Multiplatform SDK"
description: "Handle onboarding-related events in Kotlin Multiplatform using Adapty."
---
Before you start, ensure that:
1. You have installed [Adapty Kotlin Multiplatform SDK](sdk-installation-kotlin-multiplatform.md) 3.15.0 or later.
2. You have [created an onboarding](create-onboarding.md).
3. You have added the onboarding to a [placement](placements.md).
Onboardings configured with the builder generate events your app can respond to. Learn how to respond to these events below.
## Set up the onboarding event observer
To handle onboarding events, you need to implement the `AdaptyUIOnboardingsEventsObserver` interface and set it up with `AdaptyUI.setOnboardingsEventsObserver()`. This should be done early in your app's lifecycle, typically in your main activity or app initialization.
```kotlin
// In your app initialization
AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver())
```
## Custom actions
In the builder, you can add a **custom** action to a button and assign it an ID. Then, you can use this ID in your code and handle it as a custom action.
For example, if a user taps a custom button, like **Login** or **Allow notifications**, the delegate method `onCustomAction` will be triggered with the action ID from the builder. You can create your own IDs, like "allowNotifications".
```kotlin
class MyAdaptyUIOnboardingsEventsObserver : AdaptyUIOnboardingsEventsObserver {
override fun onboardingViewOnCustomAction(
view: AdaptyUIOnboardingView,
meta: AdaptyUIOnboardingMeta,
actionId: String
) {
when (actionId) {
"openPaywall" -> {
// Display paywall from onboarding
// You would typically fetch and present a new paywall here
mainUiScope.launch {
// Example: Get paywall by placement ID
// val paywallResult = Adapty.getPaywall("your_placement_id")
// paywallResult.onSuccess { paywall ->
// val paywallViewResult = AdaptyUI.createPaywallView(paywall)
// paywallViewResult.onSuccess { paywallView ->
// paywallView.present()
// }
// }
}
}
"allowNotifications" -> {
// Handle notification permissions
}
else -> {
// Handle other custom actions
}
}
}
}
// Set up the observer
AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver())
```
Event example (Click to expand)
```json
{
"actionId": "allowNotifications",
"meta": {
"onboardingId": "onboarding_123",
"screenClientId": "profile_screen",
"screenIndex": 0,
"screensTotal": 3
}
}
```
## Closing onboarding
Onboarding is considered closed when a user taps a button with the **Close** action assigned. You need to manage what happens when a user closes the onboarding. For example:
:::important
You need to manage what happens when a user closes the onboarding. For instance, you need to stop displaying the onboarding itself.
:::
```kotlin
class MyAdaptyUIOnboardingsEventsObserver : AdaptyUIOnboardingsEventsObserver {
override fun onboardingViewOnCloseAction(
view: AdaptyUIOnboardingView,
meta: AdaptyUIOnboardingMeta,
actionId: String
) {
// Dismiss the onboarding screen
mainUiScope.launch {
view.dismiss()
}
// Additional cleanup or navigation logic can be added here
// For example, navigate back or show main app content
}
}
// Set up the observer
AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver())
```
Event example (Click to expand)
```json
{
"action_id": "close_button",
"meta": {
"onboarding_id": "onboarding_123",
"screen_cid": "final_screen",
"screen_index": 3,
"total_screens": 4
}
}
```
## Opening a paywall
:::tip
Handle this event to open a paywall if you want to open it inside the onboarding. If you want to open a paywall after it is closed, there is a more straightforward way to do it β handle [`onboardingViewOnCloseAction`](#closing-onboarding) and open a paywall without relying on the event data.
:::
If a user clicks a button that opens a paywall, you will get a button action ID that you [set up manually](get-paid-in-onboardings.md). The most seamless way to work with paywalls in onboardings is to make the action ID equal to a paywall placement ID. This way, you can use the placement ID to get and open the paywall right away:
```kotlin
class MyAdaptyUIOnboardingsEventsObserver : AdaptyUIOnboardingsEventsObserver {
override fun onboardingViewOnPaywallAction(
view: AdaptyUIOnboardingView,
meta: AdaptyUIOnboardingMeta,
actionId: String
) {
// Get the paywall using the placement ID from the action
mainUiScope.launch {
val paywallResult = Adapty.getPaywall(placementId = actionId)
paywallResult.onSuccess { paywall ->
val paywallViewResult = AdaptyUI.createPaywallView(paywall)
paywallViewResult.onSuccess { paywallView ->
paywallView.present()
}.onError { error ->
// handle the error
}
}.onError { error ->
// handle the error
}
}
}
}
// Set up the observer
AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver())
```
Event example (Click to expand)
```json
{
"action_id": "premium_offer_1",
"meta": {
"onboarding_id": "onboarding_123",
"screen_cid": "pricing_screen",
"screen_index": 2,
"total_screens": 4
}
}
```
## Finishing loading onboarding
When an onboarding finishes loading, this method will be invoked:
```kotlin
class MyAdaptyUIOnboardingsEventsObserver : AdaptyUIOnboardingsEventsObserver {
override fun onboardingViewDidFinishLoading(
view: AdaptyUIOnboardingView,
meta: AdaptyUIOnboardingMeta
) {
// Handle loading completion
// You can add any initialization logic here
}
}
// Set up the observer
AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver())
```
Event example (Click to expand)
```json
{
"meta": {
"onboarding_id": "onboarding_123",
"screen_cid": "welcome_screen",
"screen_index": 0,
"total_screens": 4
}
}
```
## Navigation events
The `onboardingViewOnAnalyticsEvent` method is called when various analytics events occur during the onboarding flow.
The `event` object can be one of the following types:
|Type | Description |
|------------|-------------|
| `AdaptyOnboardingsAnalyticsEventOnboardingStarted` | When the onboarding has been loaded |
| `AdaptyOnboardingsAnalyticsEventScreenPresented` | When any screen is shown |
| `AdaptyOnboardingsAnalyticsEventScreenCompleted` | When a screen is completed. Includes optional `elementId` (identifier of the completed element) and optional `reply` (response from the user). Triggered when users perform any action to exit the screen. |
| `AdaptyOnboardingsAnalyticsEventSecondScreenPresented` | When the second screen is shown |
| `AdaptyOnboardingsAnalyticsEventUserEmailCollected` | Triggered when the user's email is collected via the input field |
| `AdaptyOnboardingsAnalyticsEventOnboardingCompleted` | Triggered when a user reaches a screen with the `final` ID. If you need this event, assign the `final` ID to the last screen. |
| `AdaptyOnboardingsAnalyticsEventUnknown` | For any unrecognized event type. Includes `name` (the name of the unknown event) and `meta` (additional metadata) |
Each event includes `meta` information containing:
| Field | Description |
|------------|-------------|
| `onboardingId` | Unique identifier of the onboarding flow |
| `screenClientId` | Identifier of the current screen |
| `screenIndex` | Current screen's position in the flow |
| `screensTotal` | Total number of screens in the flow |
Here's an example of how you can use analytics events for tracking:
```kotlin
class MyAdaptyUIOnboardingsEventsObserver : AdaptyUIOnboardingsEventsObserver {
override fun onboardingViewOnAnalyticsEvent(
view: AdaptyUIOnboardingView,
meta: AdaptyUIOnboardingMeta,
event: AdaptyOnboardingsAnalyticsEvent
) {
when (event) {
is AdaptyOnboardingsAnalyticsEventOnboardingStarted -> {
// Track onboarding start
trackEvent("onboarding_started", event.meta)
}
is AdaptyOnboardingsAnalyticsEventScreenPresented -> {
// Track screen presentation
trackEvent("screen_presented", event.meta)
}
is AdaptyOnboardingsAnalyticsEventScreenCompleted -> {
// Track screen completion with user response
trackEvent("screen_completed", event.meta, event.elementId, event.reply)
}
is AdaptyOnboardingsAnalyticsEventOnboardingCompleted -> {
// Track successful onboarding completion
trackEvent("onboarding_completed", event.meta)
}
is AdaptyOnboardingsAnalyticsEventUnknown -> {
// Handle unknown events
trackEvent(event.name, event.meta)
}
// Handle other cases as needed
}
}
private fun trackEvent(eventName: String, meta: AdaptyUIOnboardingMeta, elementId: String? = null, reply: String? = null) {
// Implement your analytics tracking here
// For example, send to your analytics service
}
}
// Set up the observer
AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver())
```
Event examples (Click to expand)
```javascript
// OnboardingStarted
{
"meta": {
"onboardingId": "onboarding_123",
"screenClientId": "welcome_screen",
"screenIndex": 0,
"screensTotal": 4
}
}
// ScreenPresented
{
"meta": {
"onboardingId": "onboarding_123",
"screenClientId": "interests_screen",
"screenIndex": 2,
"screensTotal": 4
}
}
// ScreenCompleted
{
"meta": {
"onboardingId": "onboarding_123",
"screenClientId": "profile_screen",
"screenIndex": 1,
"screensTotal": 4
},
"elementId": "profile_form",
"reply": "success"
}
// SecondScreenPresented
{
"meta": {
"onboardingId": "onboarding_123",
"screenClientId": "profile_screen",
"screenIndex": 1,
"screensTotal": 4
}
}
// UserEmailCollected
{
"meta": {
"onboardingId": "onboarding_123",
"screenClientId": "profile_screen",
"screenIndex": 1,
"screensTotal": 4
}
}
// OnboardingCompleted
{
"meta": {
"onboardingId": "onboarding_123",
"screenClientId": "final_screen",
"screenIndex": 3,
"screensTotal": 4
}
}
```
---
# File: kmp-onboarding-input
---
---
title: "Process data from onboardings in Kotlin Multiplatform SDK"
description: "Save and use data from onboardings in your Kotlin Multiplatform app with Adapty SDK."
---
When your users respond to a quiz question or input their data into an input field, the `onboardingViewOnStateUpdatedAction` method will be invoked. You can save or process the field type in your code.
For example:
```kotlin
class MyAdaptyUIOnboardingsEventsObserver : AdaptyUIOnboardingsEventsObserver {
override fun onboardingViewOnStateUpdatedAction(
view: AdaptyUIOnboardingView,
meta: AdaptyUIOnboardingMeta,
elementId: String,
params: AdaptyOnboardingsStateUpdatedParams
) {
// Store user preferences or responses
when (params) {
is AdaptyOnboardingsSelectParams -> {
// Handle single selection
val id = params.id
val value = params.value
val label = params.label
AppLogger.d("Selected option: $label (id: $id, value: $value)")
}
is AdaptyOnboardingsMultiSelectParams -> {
// Handle multiple selections
}
is AdaptyOnboardingsInputParams -> {
// Handle text input
}
is AdaptyOnboardingsDatePickerParams -> {
// Handle date selection
}
}
}
}
// Set up the observer
AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver())
```
Saved data examples (the format may differ in your implementation)
```javascript
// Example of a saved select action
{
"id": "onboarding_on_state_updated_action",
"view": { /* AdaptyUI.OnboardingView object */ },
"meta": {
"onboarding_id": "onboarding_123",
"screen_cid": "preferences_screen",
"screen_index": 1,
"total_screens": 3
},
"action": {
"element_id": "preference_selector",
"element_type": "select",
"value": {
"id": "option_1",
"value": "premium",
"label": "Premium Plan"
}
}
}
// Example of a saved multi-select action
{
"id": "onboarding_on_state_updated_action",
"view": { /* AdaptyUI.OnboardingView object */ },
"meta": {
"onboarding_id": "onboarding_123",
"screen_cid": "interests_screen",
"screen_index": 2,
"total_screens": 3
},
"action": {
"element_id": "interests_selector",
"element_type": "multi_select",
"value": [
{
"id": "interest_1",
"value": "sports",
"label": "Sports"
},
{
"id": "interest_2",
"value": "music",
"label": "Music"
}
]
}
}
// Example of a saved input action
{
"id": "onboarding_on_state_updated_action",
"view": { /* AdaptyUI.OnboardingView object */ },
"meta": {
"onboarding_id": "onboarding_123",
"screen_cid": "profile_screen",
"screen_index": 0,
"total_screens": 3
},
"action": {
"element_id": "name_input",
"element_type": "input",
"value": {
"type": "text",
"value": "John Doe"
}
}
}
// Example of a saved date picker action
{
"id": "onboarding_on_state_updated_action",
"view": { /* AdaptyUI.OnboardingView object */ },
"meta": {
"onboarding_id": "onboarding_123",
"screen_cid": "profile_screen",
"screen_index": 0,
"total_screens": 3
},
"action": {
"element_id": "birthday_picker",
"element_type": "date_picker",
"value": {
"day": 15,
"month": 6,
"year": 1990
}
}
}
```
## Use cases
### Enrich user profiles with data
If you want to immediately link the input data with the user profile and avoid asking them twice for the same info, you need to [update the user profile](kmp-setting-user-attributes.md) with the input data when handling the action.
For example, you ask users to enter their name in the text field with the `name` ID, and you want to set this field's value as user's first name. Also, you ask them to enter their email in the `email` field. In your app code, it can look like this:
```kotlin
class MyAdaptyUIOnboardingsEventsObserver : AdaptyUIOnboardingsEventsObserver {
override fun onboardingViewOnStateUpdatedAction(
view: AdaptyUIOnboardingView,
meta: AdaptyUIOnboardingMeta,
elementId: String,
params: AdaptyOnboardingsStateUpdatedParams
) {
// Store user preferences or responses
when (params) {
is AdaptyOnboardingsInputParams -> {
// Handle text input
val builder = AdaptyProfileParameters.Builder()
// Map elementId to appropriate profile field
when (elementId) {
"name" -> {
when (val input = params.input) {
is AdaptyOnboardingsTextInput -> {
builder.withFirstName(input.value)
}
}
}
"email" -> {
when (val input = params.input) {
is AdaptyOnboardingsEmailInput -> {
builder.withEmail(input.value)
}
}
}
}
// Update profile asynchronously
mainUiScope.launch {
val profileParams = builder.build()
val result = Adapty.updateProfile(profileParams)
result.onSuccess { profile ->
// Profile updated successfully
AppLogger.d("Profile updated: ${profile.email}")
}.onError { error ->
// Handle the error
AppLogger.e("Failed to update profile: ${error.message}")
}
}
}
}
}
}
// Set up the observer
AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver())
```
### Customize paywalls based on answers
Using quizzes in onboardings, you can also customize paywalls you show users after they complete the onboarding.
For example, you can ask users about their experience with sport and show different CTAs and products to different user groups.
1. [Add a quiz](onboarding-quizzes.md) in the onboarding builder and assign meaningful IDs to its options.
2. Handle the quiz responses based on their IDs and [set custom attributes](kmp-setting-user-attributes.md) for users.
```kotlin
class MyAdaptyUIOnboardingsEventsObserver : AdaptyUIOnboardingsEventsObserver {
override fun onboardingViewOnStateUpdatedAction(
view: AdaptyUIOnboardingView,
meta: AdaptyUIOnboardingMeta,
elementId: String,
params: AdaptyOnboardingsStateUpdatedParams
) {
// Handle quiz responses and set custom attributes
when (params) {
is AdaptyOnboardingsSelectParams -> {
// Handle quiz selection
val builder = AdaptyProfileParameters.Builder()
// Map quiz responses to custom attributes
when (elementId) {
"experience" -> {
// Set custom attribute 'experience' with the selected value (beginner, amateur, pro)
builder.withCustomAttribute("experience", params.value)
}
}
// Update profile asynchronously
mainUiScope.launch {
val profileParams = builder.build()
val result = Adapty.updateProfile(profileParams)
result.onSuccess { profile ->
// Profile updated successfully
AppLogger.d("Custom attribute 'experience' set to: ${params.value}")
}.onError { error ->
// Handle the error
AppLogger.e("Failed to update profile: ${error.message}")
}
}
}
}
}
}
// Set up the observer
AdaptyUI.setOnboardingsEventsObserver(MyAdaptyUIOnboardingsEventsObserver())
```
3. [Create segments](segments.md) for each custom attribute value.
4. Create a [placement](placements.md) and add [audiences](audience.md) for each segment you've created.
5. [Display a paywall](kmp-paywalls.md) for the placement in your app code. If your onboarding has a button that opens a paywall, implement the paywall code as a [response to this button's action](kmp-handling-onboarding-events.md#opening-a-paywall).
---
# File: kmp-test
---
---
title: "Test & release in Kotlin Multiplatform SDK"
description: "Learn how to check subscription status in your Kotlin Multiplatform app with Adapty."
---
If you've already implemented the Adapty SDK in your Kotlin Multiplatform app, you'll want to test that everything is set up correctly and that purchases work as expected. This involves testing both the SDK integration and the actual purchase flow with the sandbox environment.
## Test your app
For comprehensive testing of your in-app purchases, see our platform-specific testing guides: [iOS testing guide](test-purchases-in-sandbox.md) and [Android testing guide](testing-on-android.md).
## Prepare for release
Before submitting your app to the store, follow the [Release checklist](release-checklist) to confirm:
- Store connection and server notifications are configured
- Purchases complete and are reported to Adapty
- Access unlocks and restores correctly
- Privacy and review requirements are met
---
# File: kmp-reference
---
---
title: "Reference for Kotlin Multiplatform SDK"
description: "Reference documentation for Adapty Kotlin Multiplatform SDK."
---
This page contains reference documentation for Adapty Kotlin Multiplatform SDK. Choose the topic you need:
- **[SDK models](https://kmp.adapty.io/adapty/)** - Data models and structures used by the SDK
- **[Handle errors](kmp-handle-errors)** - Error handling and troubleshooting
---
# File: kmp-handle-errors
---
---
title: "Handle errors in Kotlin Multiplatform SDK"
description: "Learn how to handle errors in your Kotlin Multiplatform app with Adapty."
---
This page covers error handling in the Adapty Kotlin Multiplatform SDK.
## Error handling basics
All Adapty SDK methods return results that can be either success or error. Always handle both cases:
```kotlin showLineNumbers
Adapty.getProfile { result ->
when (result) {
is AdaptyResult.Success -> {
val profile = result.value
// Handle success
}
is AdaptyResult.Error -> {
val error = result.error
// Handle error
Log.e("Adapty", "Error: ${error.message}")
}
}
}
```
```java showLineNumbers
Adapty.getProfile(result -> {
if (result instanceof AdaptyResult.Success) {
AdaptyProfile profile = ((AdaptyResult.Success) result).getValue();
// Handle success
} else if (result instanceof AdaptyResult.Error) {
AdaptyError error = ((AdaptyResult.Error) result).getError();
// Handle error
Log.e("Adapty", "Error: " + error.getMessage());
}
});
```
## Common error codes
| Error Code | Description | Solution |
|------------|-------------|----------|
| 1000 | No product IDs found | Check product configuration in dashboard |
| 1001 | Network error | Check internet connection |
| 1002 | Invalid SDK key | Verify your SDK key |
| 1003 | Can't make payments | Device doesn't support payments |
| 1004 | Product not available | Product not configured in store |
## Handle specific errors
### Network errors
```kotlin showLineNumbers
Adapty.getPaywall("main") { result ->
when (result) {
is AdaptyResult.Success -> {
val paywall = result.value
// Use paywall
}
is AdaptyResult.Error -> {
val error = result.error
when (error.code) {
1001 -> {
// Network error - show offline message
showOfflineMessage()
}
else -> {
// Other errors
showErrorMessage(error.message)
}
}
}
}
}
```
```java showLineNumbers
Adapty.getPaywall("main", result -> {
if (result instanceof AdaptyResult.Success) {
AdaptyPaywall paywall = ((AdaptyResult.Success) result).getValue();
// Use paywall
} else if (result instanceof AdaptyResult.Error) {
AdaptyError error = ((AdaptyResult.Error) result).getError();
switch (error.getCode()) {
case 1001:
// Network error - show offline message
showOfflineMessage();
break;
default:
// Other errors
showErrorMessage(error.getMessage());
break;
}
}
});
```
### Purchase errors
```kotlin showLineNumbers
product.makePurchase { result ->
when (result) {
is AdaptyResult.Success -> {
val purchase = result.value
// Purchase successful
showSuccessMessage()
}
is AdaptyResult.Error -> {
val error = result.error
when (error.code) {
1003 -> {
// Can't make payments
showPaymentNotAvailableMessage()
}
1004 -> {
// Product not available
showProductNotAvailableMessage()
}
else -> {
// Other purchase errors
showPurchaseErrorMessage(error.message)
}
}
}
}
}
```
```java showLineNumbers
product.makePurchase(result -> {
if (result instanceof AdaptyResult.Success) {
AdaptyPurchase purchase = ((AdaptyResult.Success) result).getValue();
// Purchase successful
showSuccessMessage();
} else if (result instanceof AdaptyResult.Error) {
AdaptyError error = ((AdaptyResult.Error) result).getError();
switch (error.getCode()) {
case 1003:
// Can't make payments
showPaymentNotAvailableMessage();
break;
case 1004:
// Product not available
showProductNotAvailableMessage();
break;
default:
// Other purchase errors
showPurchaseErrorMessage(error.getMessage());
break;
}
}
});
```
## Error recovery strategies
### Retry on network errors
```kotlin showLineNumbers
fun getPaywallWithRetry(placementId: String, maxRetries: Int = 3) {
var retryCount = 0
fun attemptGetPaywall() {
Adapty.getPaywall(placementId) { result ->
when (result) {
is AdaptyResult.Success -> {
val paywall = result.value
// Use paywall
}
is AdaptyResult.Error -> {
val error = result.error
if (error.code == 1001 && retryCount < maxRetries) {
// Network error - retry
retryCount++
Handler(Looper.getMainLooper()).postDelayed({
attemptGetPaywall()
}, 1000 * retryCount) // Exponential backoff
} else {
// Max retries reached or other error
showErrorMessage(error.message)
}
}
}
}
}
attemptGetPaywall()
}
```
```java showLineNumbers
public void getPaywallWithRetry(String placementId, int maxRetries) {
AtomicInteger retryCount = new AtomicInteger(0);
Runnable attemptGetPaywall = new Runnable() {
@Override
public void run() {
Adapty.getPaywall(placementId, result -> {
if (result instanceof AdaptyResult.Success) {
AdaptyPaywall paywall = ((AdaptyResult.Success) result).getValue();
// Use paywall
} else if (result instanceof AdaptyResult.Error) {
AdaptyError error = ((AdaptyResult.Error) result).getError();
if (error.getCode() == 1001 && retryCount.get() < maxRetries) {
// Network error - retry
retryCount.incrementAndGet();
new Handler(Looper.getMainLooper()).postDelayed(this, 1000 * retryCount.get());
} else {
// Max retries reached or other error
showErrorMessage(error.getMessage());
}
}
});
}
};
attemptGetPaywall.run();
}
```
### Fallback to cached data
```kotlin showLineNumbers
class PaywallManager {
private var cachedPaywall: AdaptyPaywall? = null
fun getPaywall(placementId: String) {
Adapty.getPaywall(placementId) { result ->
when (result) {
is AdaptyResult.Success -> {
val paywall = result.value
cachedPaywall = paywall
showPaywall(paywall)
}
is AdaptyResult.Error -> {
val error = result.error
if (error.code == 1001 && cachedPaywall != null) {
// Network error - use cached paywall
showPaywall(cachedPaywall!!)
showOfflineIndicator()
} else {
// No cache available or other error
showErrorMessage(error.message)
}
}
}
}
}
}
```
```java showLineNumbers
public class PaywallManager {
private AdaptyPaywall cachedPaywall;
public void getPaywall(String placementId) {
Adapty.getPaywall(placementId, result -> {
if (result instanceof AdaptyResult.Success) {
AdaptyPaywall paywall = ((AdaptyResult.Success) result).getValue();
cachedPaywall = paywall;
showPaywall(paywall);
} else if (result instanceof AdaptyResult.Error) {
AdaptyError error = ((AdaptyResult.Error) result).getError();
if (error.getCode() == 1001 && cachedPaywall != null) {
// Network error - use cached paywall
showPaywall(cachedPaywall);
showOfflineIndicator();
} else {
// No cache available or other error
showErrorMessage(error.getMessage());
}
}
});
}
}
```
## Next steps
- [Fix for Code-1000 noProductIDsFound error](InvalidProductIdentifiers-kmp.md)
- [Fix for Code-1003 cantMakePayments error](cantMakePayments-kmp.md)
- [Complete API reference](https://kotlin.adapty.io) - Full SDK documentation
---
# File: InvalidProductIdentifiers-kmp
---
---
title: "Fix for Code-1000 noProductIDsFound error in Kotlin Multiplatform SDK"
description: "Resolve invalid product identifier errors when managing subscriptions in Adapty."
---
The 1000-code error, `noProductIDsFound`, indicates that none of the products you requested on the paywall are available for purchase in the App Store, even though they're listed there. This error may sometimes come with an `InvalidProductIdentifiers` warning. If the warning appears without an error, safely ignore it.
If you're encountering the `noProductIDsFound` error, follow these steps to resolve it:
## Step 1. Check bundle ID \{#step-2-check-bundle-id\}
---
no_index: true
---
1. Open [App Store Connect](https://appstoreconnect.apple.com/apps). Select your app and proceed to **General** β **App Information** section.
2. Copy the **Bundle ID** in the **General Information** sub-section.
3. Open the [**App settings** -> **iOS SDK** tab](https://app.adapty.io/settings/ios-sdk) from the Adapty top menu and paste the copied value to the **Bundle ID** field.
4. Get back to the **App information** page in App Store Connect and copy **Apple ID** from there.
5. On the [**App settings** -> **iOS SDK**](https://app.adapty.io/settings/ios-sdk) page in the Adapty dashboard, paste the ID to the **Apple app ID** field.
## Step 2. Check products \{#step-3-check-products\}
1. Go to **App Store Connect** and navigate to [**Monetization** β **Subscriptions**](https://appstoreconnect.apple.com/apps/6477523342/distribution/subscriptions) in the left-hand menu.
2. Click on the subscription group name. You'll see your products listed under the **Subscriptions** section.
3. Ensure the product you're testing is marked **Ready to Submit**. If not, follow the instructions on the [Product in App Store](app-store-products) page.
4. Compare the product ID from the table with the one in the [**Products**](https://app.adapty.io/products) tab in the Adapty Dashboard. If the IDs don't match, copy the product ID from the table and [create a product](create-product) with it in the Adapty Dashboard.
## Step 3. Check product availability \{#step-4-check-product-availability\}
1. Go back to **App Store Connect** and open the same **Subscriptions** section.
2. Click the subscription group name to view your products.
3. Select the product you're testing.
4. Scroll to the **Availability** section and check that all the required countries and regions are listed.
## Step 4. Check product prices \{#step-5-check-product-prices\}
1. Again, head to the **Monetization** β **Subscriptions** section in **App Store Connect**.
2. Click the subscription group name.
3. Select the product you're testing.
4. Scroll down to **Subscription Pricing** and expand the **Current Pricing for New Subscribers** section.
5. Ensure that all required prices are listed.
## Step 5. Check app paid status, bank account, and tax forms are active
1. In **App Store Connect**](https://appstoreconnect.apple.com/) homepage, click **Business**.
2. Select your company name.
3. Scroll down and check that your **Paid Apps Agreement**, **Bank Account**, and **Tax forms** all show as **Active**.
By following these steps, you should be able to resolve the `InvalidProductIdentifiers` warning and get your products live in the store
---
# File: cantMakePayments-kmp
---
---
title: "Fix for Code-1003 cantMakePayment error in Kotlin Multiplatform SDK"
description: "Resolve making payments error when managing subscriptions in Adapty."
---
The 1003 error, `cantMakePayments`, indicates that in-app purchases can't be made on this device.
If youβre encountering the `cantMakePayments` error, this is usually due to one of the reasons:
- Device restrictions: The error is not related to Adapty. See the ways to fix the issue below.
- Observer mode configuration: The `makePurchase` method and the observer mode can't be used at the same time. See the section below.
## Issue: Device restrictions
| Issue | Solution |
|---------------------------|---------------------------------------------------------|
| Screen Time restrictions | Disable In-App Purchase restrictions in [Screen Time](https://support.apple.com/en-us/102470) |
| Account suspended | Contact Apple Support to resolve account issues |
| Regional restrictions | Use App Store account from supported region |
## Issue: Using both Observer mode and makePurchase
If you are using `makePurchases` to handle purchases, you don't need to use Observer mode. [Observer mode](https://adapty.io/docs/observer-vs-full-mode) is only needed if you implement the purchase logic yourself.
So, if you're using `makePurchase`, you can safely remove enabling Observer mode from the SDK activation code.
---
# File: kmp-sdk-migration-guides
---
---
title: "Kotlin Multiplatform SDK Migration Guides"
description: "Migration guides for Adapty Kotlin Multiplatform SDK versions."
---
This page contains all migration guides for Adapty Kotlin Multiplatform SDK. Choose the version you want to migrate to for detailed instructions:
- **[Migrate to v. 3.15](migration-to-kmp-315)**
---
# File: migration-to-kmp-315
---
---
title: "Migration guide to Adapty Kotlin Multiplatform SDK 3.15.0"
description: "Migration steps for Adapty Kotlin Multiplatform SDK 3.15.0"
---
Adapty Kotlin Multiplatform SDK 3.15.0 is a major release that brings new features and improvements which, however, may require some migration steps from you.
1. Update the observer class and method names.
2. Update the fallback paywalls method name.
3. Update the view class name in event handling methods.
## Update observer class and method names
The observer class and its registration method have been renamed:
```diff
- import com.adapty.kmp.AdaptyUIObserver
+ import com.adapty.kmp.AdaptyUIPaywallsEventsObserver
- import com.adapty.kmp.models.AdaptyUIView
+ import com.adapty.kmp.models.AdaptyUIPaywallView
- class MyAdaptyUIObserver : AdaptyUIObserver {
- override fun paywallViewDidPerformAction(view: AdaptyUIView, action: AdaptyUIAction) {
+ class MyAdaptyUIPaywallsEventsObserver : AdaptyUIPaywallsEventsObserver {
+ override fun paywallViewDidPerformAction(view: AdaptyUIPaywallView, action: AdaptyUIAction) {
// handle actions
}
}
// Set up the observer
- AdaptyUI.setObserver(MyAdaptyUIObserver())
+ AdaptyUI.setPaywallsEventsObserver(MyAdaptyUIPaywallsEventsObserver())
```
## Update fallback paywalls method name
The method name for setting fallback paywalls has been changed:
```diff showLineNumbers
- Adapty.setFallbackPaywalls(assetId = "fallback.json")
+ Adapty.setFallback(assetId = "fallback.json")
.onSuccess {
// Fallback paywalls loaded successfully
}
.onError { error ->
// Handle the error
}
```
## Update view class name in event handling methods
All event handling methods now use the new `AdaptyUIPaywallView` class instead of `AdaptyUIView`:
```diff
- override fun paywallViewDidAppear(view: AdaptyUIView) {
+ override fun paywallViewDidAppear(view: AdaptyUIPaywallView) {
// Handle paywall appearance
}
- override fun paywallViewDidDisappear(view: AdaptyUIView) {
+ override fun paywallViewDidDisappear(view: AdaptyUIPaywallView) {
// Handle paywall disappearance
}
- override fun paywallViewDidSelectProduct(view: AdaptyUIPaywallView, productId: String) {
+ override fun paywallViewDidSelectProduct(view: AdaptyUIView, productId: String) {
// Handle product selection
}
- override fun paywallViewDidStartPurchase(view: AdaptyUIView, product: AdaptyPaywallProduct) {
+ override fun paywallViewDidStartPurchase(view: AdaptyUIPaywallView, product: AdaptyPaywallProduct) {
// Handle purchase start
}
- override fun paywallViewDidFinishPurchase(view: AdaptyUIView, product: AdaptyPaywallProduct, purchaseResult: AdaptyPurchaseResult) {
+ override fun paywallViewDidFinishPurchase(view: AdaptyUIPaywallView, product: AdaptyPaywallProduct, purchaseResult: AdaptyPurchaseResult) {
// Handle purchase result
}
- override fun paywallViewDidFailPurchase(view: AdaptyUIView, product: AdaptyPaywallProduct, error: AdaptyError) {
+ override fun paywallViewDidFailPurchase(view: AdaptyUIPaywallView, product: AdaptyPaywallProduct, error: AdaptyError) {
// Add your purchase failure handling logic here
}
- override fun paywallViewDidFinishRestore(view: AdaptyUIView, profile: AdaptyProfile) {
+ override fun paywallViewDidFinishRestore(view: AdaptyUIPaywallView, profile: AdaptyProfile) {
// Add your successful restore handling logic here
}
- override fun paywallViewDidFailRestore(view: AdaptyUIView, error: AdaptyError) {
+ override fun paywallViewDidFailRestore(view: AdaptyUIPaywallView, error: AdaptyError) {
// Add your restore failure handling logic here
}
- override fun paywallViewDidFinishWebPaymentNavigation(view: AdaptyUIView, product: AdaptyPaywallProduct?, error: AdaptyError?) {
+ override fun paywallViewDidFinishWebPaymentNavigation(view: AdaptyUIPaywallView, product: AdaptyPaywallProduct?, error: AdaptyError?) {
// Handle web payment navigation result
}
- override fun paywallViewDidFailLoadingProducts(view: AdaptyUIView, error: AdaptyError) {
+ override fun paywallViewDidFailLoadingProducts(view: AdaptyUIPaywallView, error: AdaptyError) {
// Add your product loading failure handling logic here
}
- override fun paywallViewDidFailRendering(view: AdaptyUIView, error: AdaptyError) {
+ override fun paywallViewDidFailRendering(view: AdaptyUIPaywallView, error: AdaptyError) {
// Handle rendering error
}
```
---
# End of Documentation
_Generated on: 2026-02-28T11:30:00.553Z_
_Successfully processed: 41/41 files_