# IOS - Adapty Documentation (Full Content)
This file contains the complete content of all documentation pages for this platform.
Generated on: 2026-03-11T12:58:11.666Z
Total files: 43
---
# File: sdk-installation-ios
---
---
title: "Install & configure iOS SDK"
description: "Step-by-step guide on installing Adapty SDK on iOS for subscription-based apps."
---
Adapty SDK includes two key modules for seamless integration into your mobile app:
- **Core Adapty**: This essential SDK is required for Adapty to function properly in your app.
- **AdaptyUI**: This optional module is needed if you use the [Adapty Paywall Builder](adapty-paywall-builder), a user-friendly, no-code tool for easily creating cross-platform paywalls.
:::tip
Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](https://github.com/adaptyteam/AdaptySDK-iOS/tree/master/Examples), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality.
:::
For a complete implementation walkthrough, you can also see the videos:
## Requirements
While the SDK technically supports iOS 13.0+ for the core module, iOS 15.0+ is effectively required for practical use since:
- All StoreKit 2 features require iOS 15.0+
- AdaptyUI module is iOS 15.0+ only
## Install Adapty SDK
[](https://github.com/adaptyteam/AdaptySDK-iOS/releases)
In Xcode, go to **File** -> **Add Package Dependency...**. Note that the steps to add package dependencies may vary between Xcode versions, so refer to Xcode documentation if needed.
1. Enter the repository URL:
```
https://github.com/adaptyteam/AdaptySDK-iOS.git
```
2. Select the version (latest stable version is recommended) and click **Add Package**.
3. In the **Choose Package Products** window, select the modules you need:
- **Adapty** (core module)
- **AdaptyUI** (optional - only if you plan to use Paywall Builder)
:::note
Note:
- To enable the [Kids mode](kids-mode.md), select **Adapty_KidsMode** instead of **Adapty**.
- Don't select any other packages from the list – you won't need them.
:::
4. Click **Add Package** to complete the installation.
5. **Verify installation:** In your project navigator, you should see "Adapty" (and "AdaptyUI" if selected) under **Package Dependencies**.
:::info
CocoaPods is now in maintenance mode, with development officially stopped. We recommend switching to [Swift Package Manager](sdk-installation-ios#install-adapty-sdk-via-swift-package-manager).
:::
1. Add Adapty to your `Podfile`. Choose the modules you need:
1. **Adapty** is the mandatory module.
2. **AdaptyUI** is an optional module you need if you plan to use the [Adapty Paywall Builder](adapty-paywall-builder).
```shell showLineNumbers title="Podfile"
pod 'Adapty'
pod 'AdaptyUI' # optional module needed only for Paywall Builder
```
2. Run:
```sh showLineNumbers title="Shell"
pod install
```
This will create a `.xcworkspace` file for your app. Use this file for all future development.
## Activate Adapty module of Adapty SDK
Activate the Adapty SDK in your app code.
:::note
The Adapty SDK only needs to be activated once in your app.
:::
To get your **Public SDK Key**:
1. Go to Adapty Dashboard and navigate to [**App settings → General**](https://app.adapty.io/settings/general).
2. From the **Api keys** section, copy the **Public SDK Key** (NOT the Secret Key).
3. Replace `"YOUR_PUBLIC_SDK_KEY"` in the code.
:::important
- Make sure you use the **Public SDK key** for Adapty initialization, the **Secret key** should be used for [server-side API](getting-started-with-server-side-api) only.
- **SDK keys** are unique for every app, so if you have multiple apps make sure you choose the right one.
:::
```swift showLineNumbers
@main
struct YourApp: App {
init() {
// Configure Adapty SDK
let configurationBuilder = AdaptyConfiguration
.builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") // Get from Adapty dashboard
Adapty.logLevel = .verbose // recommended for development and the first production release
let config = configurationBuilder.build()
// Activate Adapty SDK asynchronously
Task {
do {
try await Adapty.activate(with: config)
} catch {
// Handle error appropriately for your app
print("Adapty activation failed: ", error)
}
}
var body: some Scene {
WindowGroup {
// Your content view
}
}
}
}
```
```swift showLineNumbers
// In your AppDelegate class:
// If you only use an AppDelegate, place the following code in the
// application(_:didFinishLaunchingWithOptions:) method.
// If you use a SceneDelegate, place the following code in the
// scene(_:willConnectTo:options:) method.
Task {
do {
let configurationBuilder = AdaptyConfiguration
.builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") // Get from Adapty dashboard
.with(logLevel: .verbose) // recommended for development and the first production release
let config = configurationBuilder.build()
try await Adapty.activate(with: config)
} catch {
// Handle error appropriately for your app
print("Adapty activation failed: ", error)
}
}
```
Now set up paywalls in your app:
- If you use [Adapty Paywall Builder](adapty-paywall-builder), first [activate the AdaptyUI module](#activate-adaptyui-module-of-adapty-sdk) below, then follow the [Paywall Builder quickstart](ios-quickstart-paywalls).
- If you build your own paywall UI, see the [quickstart for custom paywalls](ios-quickstart-manual).
## Activate AdaptyUI module of Adapty SDK
If you plan to use [Paywall Builder](adapty-paywall-builder.md) and have [installed AdaptyUI module](sdk-installation-ios#install-sdks-via-cocoapods), you also need to activate AdaptyUI.
:::important
In your code, you must activate the core Adapty module before activating AdaptyUI.
:::
```swift showLineNumbers title="Swift"
@main
struct YourApp: App {
init() {
// ...ConfigurationBuilder steps
// Activate Adapty SDK asynchronously
Task {
do {
try await Adapty.activate(with: config)
try await AdaptyUI.activate()
} catch {
// Handle error appropriately for your app
print("Adapty activation failed: ", error)
}
}
// main body...
}
}
```
```swift showLineNumbers title="UIKit"
// If you only use an AppDelegate, place the following code in the
// application(_:didFinishLaunchingWithOptions:) method.
// If you use a SceneDelegate, place the following code in the
// scene(_:willConnectTo:options:) method.
Task {
do {
let configurationBuilder = AdaptyConfiguration
.builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") // Get from Adapty dashboard
.with(logLevel: .verbose) // recommended for development
let config = configurationBuilder.build()
try await Adapty.activate(with: config)
try await AdaptyUI.activate()
} catch {
// Handle error appropriately for your app
print("Adapty activation failed: ", error)
}
}
```
:::tip
Optionally, when activating AdaptyUI, you can [override default caching settings for paywalls](#set-up-media-cache-configuration-for-adaptyui).
:::
## Optional setup
### Logging
#### Set up the logging system
Adapty logs errors and other important information to help you understand what is going on. There are the following levels available:
| Level | Description |
| ---------- | ------------------------------------------------------------ |
| `error` | Only errors will be logged |
| `warn` | Errors and messages from the SDK that do not cause critical errors, but are worth paying attention to will be logged |
| `info` | Errors, warnings, and various information messages will be logged |
| `verbose` | Any additional information that may be useful during debugging, such as function calls, API queries, etc. will be logged |
```swift showLineNumbers
let configurationBuilder = AdaptyConfiguration
.builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY")
.with(logLevel: .verbose) // recommended for development
```
#### Redirect the logging system messages
If you need to send Adapty's log messages to your system or save them to a file, use the `setLogHandler` method and implement your custom logging logic inside it. This handler receives log records containing message content and severity level.
```swift showLineNumbers title="Swift"
Adapty.setLogHandler { record in
writeToLocalFile("Adapty \(record.level): \(record.message)")
}
```
### Data policies
Adapty doesn't store personal data of your users unless you explicitly send it, but you can implement additional data security policies to comply with the store or country guidelines.
#### Disable IDFA collection and sharing
When activating the Adapty module, set `idfaCollectionDisabled` to `true` to disable IDFA collection and sharing.
Use this parameter to comply with App Store Review Guidelines or avoid triggering the App Tracking Transparency prompt when IDFA isn't needed for your app. The default value is `false`. For more details on IDFA collection, refer to the [Analytics integration](analytics-integration#disable-collection-of-advertising-identifiers) section.
```swift showLineNumbers
let configurationBuilder =
AdaptyConfiguration
.builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY")
.with(idfaCollectionDisabled: true)
```
#### Disable IP collection and sharing
When activating the Adapty module, set `ipAddressCollectionDisabled` to `true` to disable user IP address collection and sharing. The default value is `false`
Use this parameter to enhance user privacy, comply with regional data protection regulations (like GDPR or CCPA), or reduce unnecessary data collection when IP-based features aren't required for your app.
```swift showLineNumbers
let configurationBuilder =
AdaptyConfiguration
.builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY")
.with(ipAddressCollectionDisabled: true)
```
#### Media cache configuration for paywalls in AdaptyUI
Please note that the AdaptyUI configuration is optional. You can activate the AdaptyUI module without its config. However, if you use the config, all parameters are required.
```swift showLineNumbers title="Swift"
// Configure AdaptyUI
let adaptyUIConfiguration = AdaptyUI.Configuration(
mediaCacheConfiguration: .init(
memoryStorageTotalCostLimit: 100 * 1024 * 1024,
memoryStorageCountLimit: .max,
diskStorageSizeLimit: 100 * 1024 * 1024
)
)
// Activate AdaptyUI
AdaptyUI.activate(configuration: adaptyUIConfiguration)
```
Parameters:
| Parameter | Presence | Description |
| :-------------------------- | :------- | :----------------------------------------------------------- |
| memoryStorageTotalCostLimit | required | Total cost limit of the storage in bytes. |
| memoryStorageCountLimit | required | The item count limit of the memory storage. |
| diskStorageSizeLimit | required | The file size limit on disk of the storage in bytes. 0 means no limit. |
### Transaction finishing behavior
:::info
This feature is available starting from SDK version 3.12.0.
:::
By default, Adapty automatically finishes transactions after successful validation. However, if you need advanced transaction validation (such as server-side receipt validation, fraud detection, or custom business logic), you can configure the SDK to use manual transaction finishing.
```swift showLineNumbers title="Swift"
let configurationBuilder = AdaptyConfiguration
.builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY")
.with(transactionsFinishBehavior: .manual) // .auto is the default
```
See more details on how to finish transactions in the [guide](ios-transaction-management).
### Clear data on backup restore
When `clearDataOnBackup` is set to `true`, the SDK detects when the app is restored from an iCloud backup and deletes all locally stored SDK data, including cached profile information, product details, and paywalls. The SDK then initializes with a clean state. Default value is `false`.
:::note
Only local SDK cache is deleted. Transaction history with Apple and user data on Adapty servers remain unchanged.
:::
```swift showLineNumbers
let configurationBuilder = AdaptyConfiguration
.builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY")
.with(clearDataOnBackup: true) // default – false
```
## Troubleshooting
#### Swift 6 concurrency error with Tuist
When building with [Tuist](https://tuist.dev/), you may see Swift 6 strict concurrency compilation errors. Typical symptoms include `@Sendable` attribute mismatches in `AdaptyUIBuilderLogic` or similar cross-module Sendability errors.
This happens because Tuist generates Xcode projects from SPM packages but doesn't preserve the `swift-tools-version: 6.0` setting. As a result, some Adapty targets (`Adapty`, `AdaptyUI`, `AdaptyUIBuilder`) compile with Swift 5 rules while others use Swift 6, creating cross-module `@Sendable` mismatches.
**Fix**: Upgrade to Adapty SDK **3.15.5** or later, which resolves the issue regardless of mixed Swift language versions.
**Workaround**: If you can't upgrade, explicitly set Swift 6 for all three Adapty targets in your Tuist configuration:
```swift showLineNumbers
targetSettings: [
"Adapty": .init().swiftVersion("6"),
"AdaptyUI": .init().swiftVersion("6"),
"AdaptyUIBuilder": .init().swiftVersion("6"),
]
```
---
# File: ios-quickstart-paywalls
---
---
title: "Enable purchases by using paywalls in iOS SDK"
description: "Quickstart guide to setting up Adapty for in-app subscription management."
---
To enable in-app purchases, you need to understand three key concepts:
- [**Products**](product.md) – anything users can buy (subscriptions, consumables, lifetime access)
- [**Paywalls**](paywalls.md) are configurations that define which products to offer. In Adapty, paywalls are the only way to retrieve products, but this design lets you modify offerings, pricing, and product combinations without touching your app code.
- [**Placements**](placements.md) – where and when you show paywalls in your app (like `main`, `onboarding`, `settings`). You set up paywalls for placements in the dashboard, then request them by placement ID in your code. This makes it easy to run A/B tests and show different paywalls to different users.
Adapty offers you three ways to enable purchases in your app. Select one of them depending on your app requirements:
| Implementation | Complexity | When to use |
|------------------------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Adapty Paywall Builder | ✅ Easy | You [create a complete, purchase-ready paywall in the no-code builder](quickstart-paywalls). Adapty automatically renders it and handles all the complex purchase flow, receipt validation, and subscription management behind the scenes. |
| Manually created paywalls | 🟡 Medium | You implement your paywall UI in your app code, but still get the paywall object from Adapty to maintain flexibility in product offerings. See the [guide](ios-quickstart-manual). |
| Observer mode | 🔴 Hard | You already have your own purchase handling infrastructure and want to keep using it. Note that the observer mode has its limitations in Adapty. See the [article](observer-vs-full-mode). |
:::important
**The steps below show how to implement a paywall created in the Adapty paywall builder.**
If you don't want to use the paywall builder, see the [guide for handling purchases in manually created paywalls](making-purchases.md).
:::
To display a paywall created in the Adapty paywall builder, in your app code, you only need to:
1. **Get the paywall**: Get the paywall from Adapty.
2. **Display the paywall and Adapty will handle purchases for you**: Show the paywall container you've got in your app.
3. **Handle button actions**: Associate user interactions with the paywall with your app's response to them. For example, open links or close the paywall when users click buttons.
## Before you start
Before you start, complete these steps:
1. [Connect your app to the App Store](initial_ios) in the Adapty Dashboard.
2. [Create your products](create-product) in Adapty.
3. [Create a paywall and add products to it](create-paywall).
4. [Create a placement and add your paywall to it](create-placement).
5. [Install and activate the Adapty SDK](sdk-installation-ios) in your app code.
:::tip
The fastest way to complete these steps is to follow the [quickstart guide](quickstart).
:::
## 1. Get the paywall created in the paywall builder
Your paywalls are associated with placements configured in the dashboard. Placements allow you to run different paywalls for different audiences or to run [A/B tests](ab-tests.md).
To get a paywall created in the Adapty paywall builder, you need to:
1. Get the `paywall` object by the [placement](placements.md) ID using the `getPaywall` method and check whether it is a paywall created in the builder.
2. Get the paywall view configuration using the `getPaywallConfiguration` method. The view configuration contains the UI elements and styling needed to display the paywall.
:::important
To get the view configuration, you must switch on the **Show on device** toggle in the Paywall Builder. Otherwise, you will get an empty view configuration, and the paywall won't be displayed.
:::
```swift
func loadPaywall() async {
let paywall = try await Adapty.getPaywall("YOUR_PLACEMENT_ID")
guard paywall.hasViewConfiguration else {
print("Paywall doesn't have view configuration")
return
}
paywallConfiguration = try await AdaptyUI.getPaywallConfiguration(forPaywall: paywall)
}
```
## 2. Display the paywall
Now, when you have the paywall configuration, it's enough to add a few lines to display your paywall.
In SwiftUI, when displaying the paywall, you also need to handle events. Some of them are optional, but `didFailPurchase`, `didFinishRestore`, `didFailRestore`, and `didFailRendering` are required. When testing, you can just copy the code from the snippet below to log these errors.
:::tip
Handling `didFinishPurchase` isn't required, but is useful when you want to perform actions after a successful purchase. If you don't implement that callback, the paywall will dismiss automatically.
:::
```swift
.paywall(
isPresented: $paywallPresented,
paywallConfiguration: paywallConfiguration,
didFailPurchase: { product, error in
print("Purchase failed: \(error)")
},
didFinishRestore: { profile in
print("Restore finished successfully")
},
didFailRestore: { error in
print("Restore failed: \(error)")
},
didFailRendering: { error in
paywallPresented = false
print("Rendering failed: \(error)")
},
showAlertItem: $alertItem
)
```
```swift
func presentPaywall(with config: AdaptyUI.PaywallConfiguration) {
let paywallController = AdaptyUI.paywallController(
with: config,
delegate: self
)
present(paywallController, animated: true)
}
```
:::info
For more details on how to display a paywall, see our [guide](ios-present-paywalls.md).
:::
## 3. Handle button actions
When users click buttons in the paywall, the iOS SDK automatically handles purchases, restoration, closing the paywall, and opening links.
However, other buttons have custom or pre-defined IDs and require handling actions in your code. Or, you may want to override their default behavior.
For example, here is the default behavior for the close button. You don't need to add it in the code, but here, you can see how it is done if needed.
:::tip
Read our guides on how to handle button [actions](handle-paywall-actions.md) and [events](ios-handling-events.md).
:::
```swift
didPerformAction: { action in
switch action {
case let .close:
paywallPresented = false // default behavior
default:
break
}
}
```
```swift
func paywallController(_ controller: AdaptyPaywallController,
didPerform action: AdaptyUI.Action) {
switch action {
case let .close:
controller.dismiss(animated: true) // default behavior
break
}
}
```
## Next steps
Your paywall is ready to be displayed in the app. [Test your purchases in sandbox mode](test-purchases-in-sandbox) to make sure you can complete a test purchase from the paywall.
Now, you need to [check the users' access level](ios-check-subscription-status.md) to ensure you display a paywall or give access to paid features to right users.
## Full example
Here is how all the steps from this guide can be integrated in your app together.
```swift
struct ContentView: View {
@State private var paywallPresented = false
@State private var alertItem: AlertItem?
@State private var paywallConfiguration: AdaptyUI.PaywallConfiguration?
@State private var isLoading = false
@State private var hasInitialized = false
var body: some View {
VStack {
if isLoading {
ProgressView("Loading...")
} else {
Text("Your App Content")
}
}
.task {
guard !hasInitialized else { return }
await initializePaywall()
hasInitialized = true
}
.paywall(
isPresented: $paywallPresented,
configuration: paywallConfiguration,
didPerformAction: { action in
switch action.type {
case let .close:
paywallPresented = false
default:
break
}
},
didFailPurchase: { product, error in
print("Purchase failed: \(error)")
},
didFinishRestore: { profile in
print("Restore finished successfully")
},
didFailRestore: { error in
print("Restore failed: \(error)")
},
didFailRendering: { error in
print("Rendering failed: \(error)")
},
showAlertItem: $alertItem
)
}
private func initializePaywall() async {
isLoading = true
defer { isLoading = false }
await loadPaywall()
paywallPresented = true
}
}
private func loadPaywall() async {
do {
let paywall = try await Adapty.getPaywall("YOUR_PLACEMENT_ID")
guard paywall.hasViewConfiguration else {
print("Paywall doesn't have view configuration")
return
}
paywallConfiguration = try await AdaptyUI.getPaywallConfiguration(forPaywall: paywall)
} catch {
print("Failed to load paywall: \(error)")
}
}
}
```
```swift
class ViewController: UIViewController {
private var paywallConfiguration: AdaptyUI.PaywallConfiguration?
override func viewDidLoad() {
super.viewDidLoad()
Task {
await initializePaywall()
}
}
private func initializePaywall() async {
do {
paywallConfiguration = try await loadPaywall()
if let paywallConfiguration {
await MainActor.run {
presentPaywall(with: paywallConfiguration)
}
}
} catch {
print("Error initializing paywall: \(error)")
}
}
private func loadPaywall() async throws -> AdaptyUI.PaywallConfiguration? {
let paywall = try await Adapty.getPaywall("YOUR_PLACEMENT_ID")
guard paywall.hasViewConfiguration else {
print("Paywall doesn't have view configuration")
return nil
}
return try await AdaptyUI.getPaywallConfiguration(forPaywall: paywall)
}
private func presentPaywall(with config: AdaptyUI.PaywallConfiguration) {
let paywallController = AdaptyUI.paywallController(with: config, delegate: self)
present(paywallController, animated: true)
}
}
// MARK: - AdaptyPaywallControllerDelegate
extension ViewController: AdaptyPaywallControllerDelegate {
func paywallController(_ controller: AdaptyPaywallController,
didPerform action: AdaptyUI.Action) {
switch action {
case let .close:
controller.dismiss(animated: true)
break
}
}
func paywallController(_ controller: AdaptyUI.PaywallController,
didFailPurchase product: AdaptyPaywallProduct,
error: AdaptyError) {
print("Purchase failed for \(product.vendorProductId): \(error)")
guard error.adaptyErrorCode != .paymentCancelled else {
return // Don't show alert for user cancellation
}
let message = switch error.adaptyErrorCode {
case .paymentNotAllowed:
"Purchases are not allowed on this device."
default:
"Purchase failed. Please try again."
}
let alert = UIAlertController(title: "Purchase Error", message: message, preferredStyle: .alert)
let okAction = UIAlertAction(title: "OK", style: .default) { _ in }
alert.addAction(okAction)
present(alert, animated: true)
}
func paywallController(_ controller: AdaptyUI.PaywallController,
didFinishRestore profile: AdaptyProfile) {
print("Restore finished successfully")
controller.dismiss(animated: true)
}
func paywallController(_ controller: AdaptyUI.PaywallController,
didFailRestore error: AdaptyError) {
print("Restore failed: \(error)")
}
func paywallController(_ controller: AdaptyUI.PaywallController,
didFailRendering error: AdaptyError) {
print("Rendering failed: \(error)")
controller.dismiss(animated: true)
}
}
```
---
# File: ios-check-subscription-status
---
---
title: "Check subscription status in iOS SDK"
description: "Learn how to check subscription status in your iOS app with Adapty."
---
To decide whether users can access paid content or see a paywall, you need to check their [access level](access-level.md) in the profile.
This article shows you how to access the profile state to decide what users need to see - whether to show them a paywall or grant access to paid features.
## Get subscription status
When you decide whether to show a paywall or paid content to a user, you check their [access level](access-level.md) in their profile. You have two options:
- Call `getProfile` if you need the latest profile data immediately (like on app launch) or want to force an update.
- Set up **automatic profile updates** to keep a local copy that's automatically refreshed whenever the subscription status changes.
:::important
By default, the `premium` access level already exists in Adapty. If you don't need to set up more than one access level, you can just use `premium`.
:::
### Get profile
The easiest way to get the subscription status is to use the `getProfile` method to access the profile:
```swift showLineNumbers
do {
let profile = try await Adapty.getProfile()
if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false {
// grant access to premium features
}
} catch {
// handle the error
}
```
```swift showLineNumbers
Adapty.getProfile { result in
if let profile = try? result.get() {
// check the access
profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false {
// grant access to premium features
}
}
}
```
### Listen to subscription updates
If you want to automatically receive profile updates in your app:
1. Conform to the `AdaptyDelegate` protocol in a type of your choice and implement the `didLoadLatestProfile` method - Adapty will automatically call this method whenever the user's subscription status changes. In the example below we use a `SubscriptionManager` type to assist with handling subscription workflows and the user's profile. This type can be injected as a dependency or set up as a singleton in a UIKit app, or added to the SwiftUI environment from the app main struct.
2. Store the updated profile data when this method is called, so you can use it throughout your app without making additional network requests.
```swift
class SubscriptionManager: AdaptyDelegate {
nonisolated func didLoadLatestProfile(_ profile: AdaptyProfile) {
let hasAccess = profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false
// Update UI, unlock content, etc.
}
}
// Set delegate after Adapty activation
Adapty.delegate = subscriptionManager
```
:::note
Adapty automatically calls `didLoadLatestProfile` when your app starts, providing cached subscription data even if the device is offline.
:::
## Connect profile with paywall logic
When you need to make immediate decisions about showing paywalls or granting access to paid features, you can check the user's profile directly. This approach is useful for scenarios like app launch, when entering premium sections, or before displaying specific content.
```swift
private func checkAccessLevel() async -> Bool {
do {
let profile = try await Adapty.getProfile()
return profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false
} catch {
print("Error checking access level: \(error)")
return false
}
}
// In your initialization logic:
let hasAccess = await checkAccessLevel()
if !hasAccess {
paywallPresented = true // Show paywall if no access
}
```
```swift
private func checkAccessLevel() async throws -> Bool {
let profile = try await Adapty.getProfile()
return profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false
}
// In your initialization logic:
let hasAccess = try await checkAccessLevel()
if !hasAccess {
presentPaywall(with: paywallConfiguration)
}
```
## Next steps
Now, when you know how to track the subscription status, [learn how to work with user profiles](ios-quickstart-identify.md) to ensure it aligns with your existing authentication system and paid access sharing permissions.
If you don't have your own authentication system, that's not a problem at all, and Adapty will manage users for you, but you can still read the [guide](ios-quickstart-identify.md) to learn how Adapty works with anonymous users.
---
# File: ios-quickstart-identify
---
---
title: "Identify users in iOS SDK"
description: "Quickstart guide to setting up Adapty for in-app subscription management."
---
:::important
This guide is for you if you have your own authentication system. Here, you will learn how to work with user profiles in Adapty to ensure it aligns with your existing authentication system.
:::
How you manage users' purchases depends on your app's authentication model:
- If your app doesn't use backend authentication and doesn't store user data, see the [section about anonymous users](#anonymous-users).
- If your app has (or will have) backend authentication, see the [section about identified users](#identified-users).
**Key concepts**:
- **Profiles** are the entities required for the SDK to work. Adapty creates them automatically.
- They can be anonymous **(without customer user ID)** or identified **(with customer user ID)**.
- You provide **customer user ID** in order to cross-reference profiles in Adapty with your internal auth system
Here is what is different for anonymous and identified users:
| | Anonymous users | Identified users |
|-------------------------|---------------------------------------------------|-------------------------------------------------------------------------|
| **Purchase management** | Store-level purchase restoration | Maintain purchase history across devices through their customer user ID |
| **Profile management** | New profiles on each reinstall | The same profile across sessions and devices |
| **Data persistence** | Anonymous users' data is tied to app installation | Identified users' data persists across app installations |
## Anonymous users
If you don't have backend authentication, **you don't need to handle authentication in the app code**:
1. When the SDK is activated on the app's first launch, Adapty **creates a new profile for the user**.
2. When the user purchases anything in the app, this purchase is **associated with their Adapty profile and their store account**.
3. When the user **re-installs** the app or installs it from a **new device**, Adapty **creates a new anonymous profile on activation**.
4. If the user has previously made purchases in your app, by default, their purchases are automatically synced from the App Store on the SDK activation.
:::note
Backup restores behave differently from reinstalls. By default, when a user restores from a backup, the SDK preserves cached data and does not create a new profile. You can configure this behavior using the `clearDataOnBackup` setting. [Learn more](sdk-installation-ios#clear-data-on-backup-restore).
:::
So, with anonymous users, new profiles will be created on each installation, but that's not a problem because, in the Adapty analytics, you can [configure what will be considered a new installation](general#4-installs-definition-for-analytics).
For anonymous users, you need to count installs by **device IDs**. In this case, each app installation on a device is counted as an install, including reinstalls.
## Identified users
You have two options to identify users in the app:
- [**During login/signup:**](#during-loginsignup) If users sign in after your app starts, call `identify()` with a customer user ID when they authenticate.
- [**During the SDK activation:**](#during-the-sdk-activation) If you already have a customer user ID stored when the app launches, send it when calling `activate()`.
:::important
By default, when Adapty receives a purchase from a Customer User ID that is currently associated with another Customer User ID, the access level is shared, so both profiles have paid access. You can configure this setting to transfer paid access from one profile to another or disable sharing completely. See the [article](general#6-sharing-purchases-between-user-accounts) for more details.
:::
### During login/signup
If you're identifying users after the app launch (for example, after they log into your app or sign up), use the `identify` method to set their customer user ID.
- If you **haven't used this customer user ID before**, Adapty will automatically link it to the current profile.
- If you **have used this customer user ID to identify the user before**, Adapty will switch to working with the profile associated with this customer user ID.
:::important
Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one.
:::
```swift showLineNumbers
do {
try await Adapty.identify("YOUR_USER_ID") // Unique for each user
} catch {
// handle the error
}
```
```swift showLineNumbers
// User IDs must be unique for each user
Adapty.identify("YOUR_USER_ID") { error in
if let error {
// handle the error
}
}
```
### During the SDK activation
If you already know a customer user ID when you activate the SDK, you can send it in the `activate` method instead of calling `identify` separately.
If you know a customer user ID but set it only after the activation, that will mean that, upon activation, Adapty will create a new anonymous profile and switch to the existing one only after you call `identify`.
You can pass either an existing customer user ID (the one you have used before) or a new one. If you pass a new one, a new profile created upon activation will be automatically linked to the customer user ID.
:::note
By default, creating anonymous profiles does not affect analytics dashboards, because installs are counted based on device IDs.
A device ID represents a single installation of the app from the store on a device and is regenerated only after the app is reinstalled.
It does not depend on whether this is a first or repeated installation, or whether an existing customer user ID is used.
Creating a profile (on SDK activation or logout), logging in, or upgrading the app without reinstalling the app does not generate additional install events.
If you want to count installs based on unique users rather than devices, go to **App settings** and configure [**Installs definition for analytics**](general#4-installs-definition-for-analytics).
:::
```swift showLineNumbers
// Place in the app main struct for SwiftUI or in AppDelegate for UIKit
let configurationBuilder =
AdaptyConfiguration
.builder(withAPIKey: "PUBLIC_SDK_KEY")
.with(customerUserId: "YOUR_USER_ID") // Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one.
do {
try await Adapty.activate(with: configurationBuilder.build())
} catch {
// handle the error
}
```
```swift showLineNumbers
// Place in the app main struct for SwiftUI or in AppDelegate for UIKit
let configurationBuilder =
AdaptyConfiguration
.builder(withAPIKey: "PUBLIC_SDK_KEY")
.with(customerUserId: "YOUR_USER_ID") // Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one.
Adapty.activate(with: configurationBuilder.build()) { error in
// handle the error
}
```
### Log users out
If you have a button for logging users out, use the `logout` method.
:::important
Logging users out creates a new anonymous profile for the user.
:::
```swift showLineNumbers
do {
try await Adapty.logout()
} catch {
// handle the error
}
```
```swift showLineNumbers
Adapty.logout { error in
if error == nil {
// successful logout
}
}
```
:::info
To log users back into the app, use the `identify` method.
:::
### Allow purchases without login
If your users can make purchases both before and after they log into your app, you need to ensure that they will keep access after they log in:
1. When a logged-out user makes a purchase, Adapty ties it to their anonymous profile ID.
2. When the user logs into their account, Adapty switches to working with their identified profile.
- If it is a new customer user ID (e.g., the purchase has been made before registration), Adapty assigns the customer user ID to the current profile, so all the purchase history is maintained.
- If it is an existing customer user ID (the customer user ID is already linked to a profile), you need to get the actual access level after the profile switch. You can either call [`getProfile`](ios-check-subscription-status.md) right after the identification, or [listen for profile updates](ios-check-subscription-status.md) so the data syncs automatically.
## Next steps
Congratulations! You have implemented in-app payment logic in your app! We wish you all the best with your app monetization!
To get even more from Adapty, you can explore these topics:
- [**Testing**](test-purchases-in-sandbox.md): Ensure that everything works as expected
- [**Onboardings**](ios-onboardings.md): Engage users with onboardings and drive retention
- [**Integrations**](configuration.md): Integrate with marketing attribution and analytics services in just one line of code
- [**Set custom profile attributes**](setting-user-attributes.md): Add custom attributes to user profiles and create segments, so you can launch A/B tests or show different paywalls to different users
---
# File: adapty-cursor
---
---
title: "Integrate Adapty into your iOS app with AI assistance"
description: "A step-by-step guide to integrating Adapty into your iOS app using Cursor, Context7, ChatGPT, Claude, or other AI tools."
---
This guide helps you integrate Adapty into your iOS app with the help of an LLM. You'll start by preparing your Adapty dashboard, then work through each implementation stage by sending focused doc links to your LLM. At the end, you'll find best practices for setting up your AI tools with Adapty documentation.
:::tip
Copy this entire page as Markdown and paste it into your LLM to get started — click **Copy for LLM** at the top of the page or open [the .md version](https://adapty.io/docs/adapty-cursor.md). The LLM will use the guide links and checkpoints to walk you through each stage.
:::
## Before you start: dashboard checklist
Adapty requires dashboard configuration before you write any SDK code. Your LLM cannot look up dashboard values for you — you'll need to provide them.
### Required before coding
1. **Connect your app store**: In the Adapty Dashboard, go to **App settings → General**. This is required for purchases to work.
[Connect App Store](integrate-payments.md)
2. **Copy your Public SDK key**: In the Adapty Dashboard, go to **App settings → General**, then find the **API keys** section. In code, this is the string you pass to `Adapty.activate("YOUR_PUBLIC_SDK_KEY")`.
3. **Create at least one product**: In the Adapty Dashboard, go to the **Products** page. You don't reference products directly in code — Adapty delivers them through paywalls.
[Add products](quickstart-products.md)
4. **Create a paywall and a placement**: In the Adapty Dashboard, create a paywall on the **Paywalls** page, then assign it to a placement on the **Placements** page. In code, the placement ID is the string you pass to `Adapty.getPaywall("YOUR_PLACEMENT_ID")`.
[Create paywall](quickstart-paywalls.md)
5. **Set up access levels**: In the Adapty Dashboard, configure per product on the **Products** page. In code, the string checked in `profile.accessLevels["premium"]`. The default `premium` access level works for most apps. If paying users get access to different features depending on the product (for example, a `basic` plan vs. a `pro` plan), [create additional access levels](assigning-access-level-to-a-product.md) before you start coding.
:::tip
Once you have all five, you're ready to write code. Tell your LLM: "My Public SDK key is X, my placement ID is Y" so it can generate correct initialization and paywall-fetching code.
:::
### Set up when ready
These are not required to start coding, but you'll want them as your integration matures:
- **A/B tests**: Configure on the **Placements** page. No code change needed.
[A/B tests](ab-tests.md)
- **Additional paywalls and placements**: Add more `getPaywall` calls with different placement IDs.
- **Analytics integrations**: Configure on the **Integrations** page. Setup varies by integration. See [analytics integrations](analytics-integration.md) and [attribution integrations](attribution-integration.md).
## Feed Adapty docs to your LLM
### Use Context7 (recommended)
[Context7](https://context7.com) is an MCP server that gives your LLM direct access to up-to-date Adapty documentation. Your LLM fetches the right docs automatically based on what you ask — no manual URL pasting needed.
Context7 works with **Cursor**, **Claude Code**, **Windsurf**, and other MCP-compatible tools. To set it up, run:
```
npx ctx7 setup
```
This detects your editor and configures the Context7 server. For manual setup, see the [Context7 GitHub repository](https://github.com/upstash/context7).
Once configured, reference the Adapty library in your prompts:
```
Use the adaptyteam/adapty-docs library to look up how to install the iOS SDK
```
:::warning
Even though Context7 removes the need to paste doc links manually, the implementation order matters. Follow the [implementation walkthrough](#implementation-walkthrough) below step by step to make sure everything works.
:::
### Use plain text docs
You can access any Adapty doc as plain text Markdown. Add `.md` to the end of its URL, or click **Copy for LLM** under the article title. For example: [adapty-cursor.md](https://adapty.io/docs/adapty-cursor.md).
Each stage in the [implementation walkthrough](#implementation-walkthrough) below includes a "Send this to your LLM" block with `.md` links to paste.
For more documentation at once, see [index files and platform-specific subsets](#plain-text-doc-index-files) below.
## Implementation walkthrough
The rest of this guide walks through Adapty integration in implementation order. Each stage includes the docs to send to your LLM, what you should see when done, and common issues.
### Plan your integration
Before jumping into code, ask your LLM to analyze your project and create an implementation plan. If your AI tool supports a planning mode (like Cursor's or Claude Code's plan mode), use it so the LLM can read both your project structure and the Adapty docs before writing any code.
Tell your LLM which approach you use for purchases — this affects the guides it should follow:
- [**Adapty Paywall Builder**](adapty-paywall-builder.md): You create paywalls in Adapty's no-code builder, and the SDK renders them automatically.
- [**Manually created paywalls**](making-purchases.md): You build your own paywall UI in code but still use Adapty to fetch products and handle purchases.
- [**Observer mode**](observer-vs-full-mode.md): You keep your existing purchase infrastructure and use Adapty only for analytics and integrations.
Not sure which one to pick? Read the [comparison table in the quickstart](ios-quickstart-paywalls.md).
### Install and configure the SDK
Install the Adapty SDK package via Swift Package Manager in Xcode and activate it with your Public SDK key. This is the foundation — nothing else works without it.
**Guide:** [Install & configure Adapty SDK](sdk-installation-ios.md)
Send this to your LLM:
```
Read these Adapty docs before writing code:
- https://adapty.io/docs/sdk-installation-ios.md
```
:::tip[Checkpoint]
- **Expected:** App builds and runs. Xcode console shows Adapty activation log.
- **Gotcha:** "Public API key is missing" → check you replaced the placeholder with your real key from App settings.
:::
### Show paywalls and handle purchases
Fetch a paywall by placement ID, display it, and handle purchase events. The guides you need depend on how you handle purchases.
Test each purchase in the sandbox as you go — don't wait until the end. See [Test purchases in sandbox](test-purchases-in-sandbox.md) for setup instructions.
**Guides:**
- [Enable purchases using paywalls (quickstart)](ios-quickstart-paywalls.md)
- [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls.md)
- [Display paywalls](ios-present-paywalls.md)
- [Handle paywall events](ios-handling-events.md)
- [Respond to button actions](handle-paywall-actions.md)
Send this to your LLM:
```
Read these Adapty docs before writing code:
- https://adapty.io/docs/ios-quickstart-paywalls.md
- https://adapty.io/docs/get-pb-paywalls.md
- https://adapty.io/docs/ios-present-paywalls.md
- https://adapty.io/docs/ios-handling-events.md
- https://adapty.io/docs/handle-paywall-actions.md
```
:::tip[Checkpoint]
- **Expected:** Paywall appears with your configured products. Tapping a product triggers the sandbox purchase dialog.
- **Gotcha:** Empty paywall or `getPaywall` error → verify placement ID matches the dashboard exactly and the placement has an audience assigned.
:::
**Guides:**
- [Enable purchases in your custom paywall (quickstart)](ios-quickstart-manual.md)
- [Fetch paywalls and products](fetch-paywalls-and-products.md)
- [Render paywall designed by remote config](present-remote-config-paywalls.md)
- [Make purchases](making-purchases.md)
- [Restore purchases](restore-purchase.md)
Send this to your LLM:
```
Read these Adapty docs before writing code:
- https://adapty.io/docs/ios-quickstart-manual.md
- https://adapty.io/docs/fetch-paywalls-and-products.md
- https://adapty.io/docs/present-remote-config-paywalls.md
- https://adapty.io/docs/making-purchases.md
- https://adapty.io/docs/restore-purchase.md
```
:::tip[Checkpoint]
- **Expected:** Your custom paywall displays products fetched from Adapty. Tapping a product triggers the sandbox purchase dialog.
- **Gotcha:** Empty products array → verify the paywall has products assigned in the dashboard and the placement has an audience.
:::
**Guides:**
- [Observer mode overview](observer-vs-full-mode.md)
- [Implement Observer mode](implement-observer-mode.md)
- [Report transactions in Observer mode](report-transactions-observer-mode.md)
Send this to your LLM:
```
Read these Adapty docs before writing code:
- https://adapty.io/docs/observer-vs-full-mode.md
- https://adapty.io/docs/implement-observer-mode.md
- https://adapty.io/docs/report-transactions-observer-mode.md
```
:::tip[Checkpoint]
- **Expected:** After a sandbox purchase using your existing purchase flow, the transaction appears in the Adapty dashboard **Event Feed**.
- **Gotcha:** No events → verify you're reporting transactions to Adapty and App Store Server Notifications are configured.
:::
### Check subscription status
After a purchase, check the user profile for an active access level to gate premium content.
**Guide:** [Check subscription status](ios-check-subscription-status.md)
Send this to your LLM:
```
Read these Adapty docs before writing code:
- https://adapty.io/docs/ios-check-subscription-status.md
```
:::tip[Checkpoint]
- **Expected:** After a sandbox purchase, `profile.accessLevels["premium"]?.isActive` returns `true`.
- **Gotcha:** Empty `accessLevels` after purchase → check the product has an access level assigned in the dashboard.
:::
### Identify users
Link your app user accounts to Adapty profiles so purchases persist across devices.
:::important
Skip this step if your app has no authentication.
:::
**Guide:** [Identify users](ios-quickstart-identify.md)
Send this to your LLM:
```
Read these Adapty docs before writing code:
- https://adapty.io/docs/ios-quickstart-identify.md
```
:::tip[Checkpoint]
- **Expected:** After calling `Adapty.identify("your-user-id")`, the dashboard **Profiles** section shows your custom user ID.
- **Gotcha:** Call `identify` after activation but before fetching paywalls to avoid anonymous profile attribution.
:::
### Prepare for release
Once your integration works in the sandbox, walk through the release checklist to make sure everything is production-ready.
**Guide:** [Release checklist](release-checklist.md)
Send this to your LLM:
```
Read these Adapty docs before releasing:
- https://adapty.io/docs/release-checklist.md
```
:::tip[Checkpoint]
- **Expected:** All checklist items confirmed: store connection, server notifications, purchase flow, access level checks, and privacy requirements.
- **Gotcha:** Missing App Store Server Notifications → configure them in **App settings → iOS SDK** or events won't appear in the dashboard.
:::
## Plain text doc index files
If you need to give your LLM broader context beyond individual pages, we host index files that list or combine all Adapty documentation:
- [`llms.txt`](https://adapty.io/docs/llms.txt): Lists all pages with `.md` links. An [emerging standard](https://llmstxt.org/) for making websites accessible to LLMs. Note that for some AI agents (e.g., ChatGPT) you will need to download `llms.txt` and upload it to the chat as a file.
- [`llms-full.txt`](https://adapty.io/docs/llms-full.txt): The entire Adapty documentation site combined into a single file. Very large — use only when you need the full picture.
- iOS-specific [`ios-llms.txt`](https://adapty.io/docs/ios-llms.txt) and [`ios-llms-full.txt`](https://adapty.io/docs/ios-llms-full.txt): Platform-specific subsets that save tokens compared to the full site.
---
# File: get-pb-paywalls
---
---
title: "Fetch Paywall Builder paywalls and their configuration in iOS SDK"
description: "Learn how to retrieve PB paywalls in Adapty for better subscription control in your iOS app."
---
After [you designed the visual part for your paywall](adapty-paywall-builder) with the new Paywall Builder in the Adapty Dashboard, you can display it in your mobile app. The first step in this process is to get the paywall associated with the placement and its view configuration as described below.
:::warning
The new Paywall Builder works with iOS SDK version 3.0 or higher. For presenting paywalls in Adapty SDK v2 designed with the legacy Paywall Builder, see [Display paywalls designed with legacy Paywall Builder](adapty-paywall-builder.md).
:::
Please be aware that this topic refers to Paywall Builder-customized paywalls. If you are implementing your paywalls manually, please refer to the [Fetch paywalls and products for remote config paywalls in your mobile app](fetch-paywalls-and-products) topic.
:::tip
Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality.
:::
Before you start displaying paywalls in your mobile app (click to expand)
1. [Create your products](create-product) in the Adapty Dashboard.
2. [Create a paywall and incorporate the products into it](create-paywall) in the Adapty Dashboard.
3. [Create placements and incorporate your paywall into it](create-placement) in the Adapty Dashboard.
4. Install [Adapty SDK](sdk-installation-ios) in your mobile app.
## Fetch paywall designed with Paywall Builder
If you've [designed a paywall using the Paywall Builder](adapty-paywall-builder), you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. Nevertheless, you need to get its ID via the placement, its view configuration, and then present it in your mobile app.
To ensure optimal performance, it's crucial to retrieve the paywall and its [view configuration](get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) as early as possible, allowing sufficient time for images to download before presenting them to the user.
To get a paywall, use the `getPaywall` method:
```swift showLineNumbers
do {
let paywall = try await Adapty.getPaywall("YOUR_PLACEMENT_ID")
// the requested paywall
} catch {
// handle the error
}
```
```swift showLineNumbers
Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en") { result in
switch result {
case let .success(paywall):
// the requested paywall
case let .failure(error):
// handle the error
}
}
```
Parameters:
| Parameter | Presence | Description |
|---------|--------|-----------|
| **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. |
| **locale** |
optional
default: `en`
|
The identifier of the [paywall localization](add-paywall-locale-in-adapty-paywall-builder). This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores paywalls locally in two layers: regularly updated cache described above and [fallback paywalls](fallback-paywalls). We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.
|
| **loadTimeout** | default: 5 sec |
This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.
Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.
|
Response parameters:
| Parameter | Description |
| :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Paywall | An [`AdaptyPaywall`](https://swift.adapty.io/documentation/adapty/adaptypaywall) object with a list of product IDs, the paywall identifier, remote config, and several other properties. |
## Fetch the view configuration of paywall designed using Paywall Builder
:::important
Make sure to enable the **Show on device** toggle in the paywall builder. If this option isn't turned on, the view configuration won't be available to retrieve.
:::
After fetching the paywall, check if it includes a view configuration, which indicates that it was created using Paywall Builder. This will guide you on how to display the paywall. If the view configuration is present, treat it as a Paywall Builder paywall; if not, [handle it as a remote config paywall](present-remote-config-paywalls).
Use the `getPaywallConfiguration` method to load the view configuration.
```swift showLineNumbers
guard paywall.hasViewConfiguration else {
// use your custom logic
return
}
do {
let paywallConfiguration = try await AdaptyUI.getPaywallConfiguration(
forPaywall: paywall,
products: products
)
// use loaded configuration
} catch {
// handle the error
}
```
Parameters:
| Parameter | Presence | Description |
| :----------------------- | :------------- | :----------------------------------------------------------- |
| **paywall** | required | An `AdaptyPaywall` object to obtain a controller for the desired paywall. |
| **loadTimeout** | default: 5 sec | This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood. |
| **products** | optional | Provide an array of `AdaptyPaywallProducts` to optimize the display timing of products on the screen. If `nil` is passed, AdaptyUI will automatically fetch the necessary products. |
:::note
If you are using multiple languages, learn how to add a [Paywall Builder localization](add-paywall-locale-in-adapty-paywall-builder) and how to use locale codes correctly [here](localizations-and-locale-codes).
:::
Once loaded, [present the paywall](ios-present-paywalls).
## Get a paywall for a default audience to fetch it faster
Typically, paywalls are fetched almost instantly, so you don't need to worry about speeding up this process. However, in cases where you have numerous audiences and paywalls, and your users have a weak internet connection, fetching a paywall may take longer than you'd like. In such situations, you might want to display a default paywall to ensure a smooth user experience rather than showing no paywall at all.
To address this, you can use the `getPaywallForDefaultAudience` method, which fetches the paywall of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the paywall by the `getPaywall` method, as detailed in the [Fetch Paywall Information](get-pb-paywalls#fetch-paywall-designed-with-paywall-builder) section above.
:::warning
Why we recommend using `getPaywall`
The `getPaywallForDefaultAudience` method comes with a few significant drawbacks:
- **Potential backward compatibility issues**: If you need to show different paywalls for different app versions (current and future), you may face challenges. You'll either have to design paywalls that support the current (legacy) version or accept that users with the current (legacy) version might encounter issues with non-rendered paywalls.
- **Loss of targeting**: All users will see the same paywall designed for the **All Users** audience, which means you lose personalized targeting (including based on countries, marketing attribution or your own custom attributes).
If you're willing to accept these drawbacks to benefit from faster paywall fetching, use the `getPaywallForDefaultAudience` method as follows. Otherwise stick to `getPaywall` described [above](get-pb-paywalls#fetch-paywall-designed-with-paywall-builder).
:::
```swift showLineNumbers
Adapty.getPaywallForDefaultAudience(placementId: "YOUR_PLACEMENT_ID", locale: "en") { result in
switch result {
case let .success(paywall):
// the requested paywall
case let .failure(error):
// handle the error
}
}
```
:::note
The `getPaywallForDefaultAudience` method is available starting from iOS SDK version 2.11.2.
:::
| Parameter | Presence | Description |
|---------|--------|-----------|
| **placementId** | required | The identifier of the [Placement](placements). This is the value you specified when creating a placement in your Adapty Dashboard. |
| **locale** |
optional
default: `en`
|
The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
|
## Customize assets
To customize images and videos in your paywall, implement the custom assets.
Hero images and videos have predefined IDs: `hero_image` and `hero_video`. In a custom asset bundle, you target these elements by their IDs and customize their behavior.
For other images and videos, you need to [set a custom ID](https://adapty.io/docs/custom-media) in the Adapty dashboard.
For example, you can:
- Show a different image or video to some users.
- Show a local preview image while a remote main image is loading.
- Show a preview image before running a video.
:::important
To use this feature, update the Adapty iOS SDK to version 3.7.0 or higher.
:::
Here's an example of how you can provide custom assets via a simple dictionary:
```swift showLineNumbers
let customAssets: [String: AdaptyCustomAsset] = [
// Show a local image using a custom ID
"custom_image": .image(
.uiImage(value: UIImage(named: "image_name")!)
),
// Show a local preview image while a remote main image is loading
"hero_image": .image(
.remote(
url: URL(string: "https://example.com/image.jpg")!,
preview: UIImage(named: "preview_image")
)
),
// Show a local video with a preview image
"hero_video": .video(
.file(
url: Bundle.main.url(forResource: "custom_video", withExtension: "mp4")!,
preview: .uiImage(value: UIImage(named: "video_preview")!)
)
),
]
let paywallConfig = try await AdaptyUI.getPaywallConfiguration(
forPaywall: paywall,
assetsResolver: customAssets
)
```
:::note
If an asset is not found, the paywall will fall back to its default appearance.
:::
## Set up developer-defined timers
To use custom timers in your mobile app, create an object that follows the `AdaptyTimerResolver` protocol. This object defines how each custom timer should be rendered. If you prefer, you can use a `[String: Date]` dictionary directly, as it already conforms to this protocol. Here is an example:
```swift showLineNumbers
@MainActor
struct AdaptyTimerResolverImpl: AdaptyTimerResolver {
func timerEndAtDate(for timerId: String) -> Date {
switch timerId {
case "CUSTOM_TIMER_6H":
Date(timeIntervalSinceNow: 3600.0 * 6.0) // 6 hours
case "CUSTOM_TIMER_NY":
Calendar.current.date(from: DateComponents(year: 2025, month: 1, day: 1)) ?? Date(timeIntervalSinceNow: 3600.0)
default:
Date(timeIntervalSinceNow: 3600.0) // 1 hour
}
}
}
```
In this example, `CUSTOM_TIMER_NY` and `CUSTOM_TIMER_6H` are the **Timer ID**s of developer-defined timers you set in the Adapty Dashboard. The `timerResolver` ensures your app dynamically updates each timer with the correct value. For example:
- `CUSTOM_TIMER_NY`: The time remaining until the timer's end, such as New Year's Day.
- `CUSTOM_TIMER_6H`: The time left in a 6-hour period that started when the user opened the paywall.
---
# File: ios-present-paywalls
---
---
title: "Present new Paywall Builder paywalls in iOS SDK"
description: "Discover how to present paywalls on iOS to boost conversions and revenue."
---
If you've customized a paywall using the Paywall Builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown.
:::warning
This guide is for **[new Paywall Builder paywalls](adapty-paywall-builder.md)** . The process for presenting paywalls differs for paywalls designed with different versions of Paywall Builder, remote config paywalls, and [Observer mode](observer-vs-full-mode).
- For presenting **Legacy Paywall Builder paywalls**, check out [iOS - Present legacy Paywall Builder paywalls](ios-present-paywalls-legacy).
- For presenting **Remote config paywalls**, see [Render paywall designed by remote config](present-remote-config-paywalls).
- For presenting **Observer mode paywalls**, see [iOS - Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode)
:::
To get the `AdaptyUI.PaywallConfiguration` object used below, see [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls).
## Present paywalls in SwiftUI
### Present as a modal view
In order to display the visual paywall on the device screen as a modal view, use the `.paywall` modifier in SwiftUI:
```swift showLineNumbers title="SwiftUI"
@State var paywallPresented = false // ensure that you manage this variable state and set it to `true` at the moment you want to show the paywall
var body: some View {
Text("Hello, AdaptyUI!")
.paywall(
isPresented: $paywallPresented,
paywallConfiguration: ,
didPerformAction: { action in
switch action {
case .close:
paywallPresented = false
default:
// Handle other actions
break
}
},
didFinishPurchase: { product, profile in paywallPresented = false },
didFailPurchase: { product, error in /* handle the error */ },
didFinishRestore: { profile in /* check access level and dismiss */ },
didFailRestore: { error in /* handle the error */ },
didFailRendering: { error in paywallPresented = false }
)
}
```
Parameters:
| Parameter | Required | Description |
|:----------------------------------|:---------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **isPresented** | required | A binding that manages whether the paywall screen is displayed. |
| **paywallConfiguration** | required | An `AdaptyUI.PaywallConfiguration` object containing visual details of the paywall. Use the `AdaptyUI.paywallConfiguration(for:products:viewConfiguration:observerModeResolver:tagResolver:timerResolver:)` method. Refer to [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls) topic for more details. |
| **didFailPurchase** | required | Invoked when `Adapty.makePurchase()` fails. |
| **didFinishRestore** | required | Invoked when `Adapty.restorePurchases()` completes successfully. |
| **didFailRestore** | required | Invoked when `Adapty.restorePurchases()` fails. |
| **didFailRendering** | required | Invoked if an error occurs while rendering the interface. In this case, [contact Adapty Support](mailto:support@adapty.io). |
| **fullScreen** | optional | Determines if the paywall appears in full-screen mode or as a modal. Defaults to `true`. |
| **didAppear** | optional | Invoked when the paywall view was presented. |
| **didDisappear** | optional | Invoked when the paywall view was dismissed. |
| **didPerformAction** | optional | Invoked when a user clicks a button. Different buttons have different action IDs. Two action IDs are pre-defined: `close` and `openURL`, while others are custom and can be set in the builder. |
| **didSelectProduct** | optional | If the product was selected for purchase (by a user or by the system), this callback will be invoked. |
| **didStartPurchase** | optional | Invoked when the user begins the purchase process. |
| **didFinishPurchase** | optional | Invoked when `Adapty.makePurchase()` completes successfully. |
| **didFinishWebPaymentNavigation** | optional | Invoked when web payment navigation finishes. |
| **didStartRestore** | optional | Invoked when the user starts the restore process. |
| **didFailLoadingProducts** | optional | Invoked when errors occur during product loading. Return `true` to retry loading. |
| **didPartiallyLoadProducts** | optional | Invoked when products are partially loaded. |
| **showAlertItem** | optional | A binding that manages the display of alert items above the paywall. |
| **showAlertBuilder** | optional | A function for rendering the alert view. |
| **placeholderBuilder** | optional | A function for rendering the placeholder view while the paywall is loading. |
Refer to the [iOS - Handling events](ios-handling-events) topic for more details on parameters.
### Present as a non-modal view
You can also present paywalls as navigation destinations or inline views within your app's navigation flow. Use `AdaptyPaywallView` directly in your SwiftUI views:
```swift showLineNumbers title="SwiftUI"
AdaptyPaywallView(
paywallConfiguration: ,
didFailPurchase: { product, error in
// Handle purchase failure
},
didFinishRestore: { profile in
// Handle successful restore
},
didFailRestore: { error in
// Handle restore failure
},
didFailRendering: { error in
// Handle rendering error
}
)
```
## Present paywalls in UIKit
In order to display the visual paywall on the device screen, do the following:
1. Initialize the visual paywall you want to display by using the `.paywallController(for:products:viewConfiguration:delegate:)` method:
```swift showLineNumbers title="Swift"
import Adapty
import AdaptyUI
let visualPaywall = AdaptyUI.paywallController(
with: ,
delegate:
)
```
Request parameters:
| Parameter | Presence | Description |
| :----------------------- | :------- | :---------- |
| **paywall configuration** | required | An `AdaptyUI.PaywallConfiguration` object containing visual details of the paywall. Use the `AdaptyUI.getPaywallConfiguration(forPaywall:locale:)` method. Refer to [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls) topic for more details. |
| **delegate** | required | An `AdaptyPaywallControllerDelegate` to listen to paywall events. Refer to [Handling paywall events](ios-handling-events) topic for more details.
Returns:
| Object | Description |
| :---------------------- | :--------------------------------------------------- |
| **AdaptyPaywallController** | An object, representing the requested paywall screen |
2. After the object has been successfully created, you can display it on the screen of the device:
```swift showLineNumbers title="Swift"
present(visualPaywall, animated: true)
```
:::tip
Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality.
:::
---
# File: handle-paywall-actions
---
---
title: "Respond to button actions in iOS SDK"
description: "Handle paywall button actions in iOS using Adapty for better app monetization."
---
If you are building paywalls using the Adapty paywall builder, it's crucial to set up buttons properly:
1. Add a [button in the paywall builder](paywall-buttons.md) and assign it either a pre-existing action or create a custom action ID.
2. Write code in your app to handle each action you've assigned.
This guide shows how to handle custom and pre-existing actions in your code.
:::warning
**Only purchases, restorations, paywall closures, and URL opening are handled automatically.** All other button actions require proper response implementation in the app code.
:::
## Close paywalls
To add a button that will close your paywall:
1. In the paywall builder, add a button and assign it the **Close** action.
2. In your app code, implement a handler for the `close` action that dismisses the paywall.
:::info
In the iOS SDK, the `close` action triggers closing the paywall by default. However, you can override this behavior in your code if needed. For example, closing one paywall might trigger opening another.
:::
```swift
func paywallController(_ controller: AdaptyPaywallController,
didPerform action: AdaptyUI.Action) {
switch action {
case .close:
controller.dismiss(animated: true) // default behavior
break
}
}
```
## Open URLs from paywalls
:::tip
If you want to add a group of links (e.g., terms of use and purchase restoration), add a **Link** element in the paywall builder and handle it the same way as buttons with the **Open URL** action.
:::
To add a button that opens a link from your paywall (e.g., **Terms of use** or **Privacy policy**):
1. In the paywall builder, add a button, assign it the **Open URL** action, and enter the URL you want to open.
2. In your app code, implement a handler for the `openUrl` action that opens the received URL in a browser.
:::info
In the iOS SDK, the `openUrl` action triggers opening the URL by default. However, you can override this behavior in your code if needed.
:::
```swift
func paywallController(_ controller: AdaptyPaywallController,
didPerform action: AdaptyUI.Action) {
switch action {
case let .openURL(url):
UIApplication.shared.open(url, options: [:]) // default behavior
break
}
}
```
## Log into the app
To add a button that logs users into your app:
1. In the paywall builder, add a button and assign it the **Login** action.
2. In your app code, implement a handler for the `login` action that identifies your user.
```swift
func paywallController(_ controller: AdaptyPaywallController,
didPerform action: AdaptyUI.Action) {
switch action {
case .login:
// Show a login screen
let loginVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "LoginViewController")
controller.present(loginVC, animated: true)
}
}
```
## Handle custom actions
To add a button that handles any other actions:
1. In the paywall builder, add a button, assign it the **Custom** action, and assign it an ID.
2. In your app code, implement a handler for the action ID you've created.
For example, if you have another set of subscription offers or one-time purchases, you can add a button that will display another paywall:
```swift
func paywallController(_ controller: AdaptyPaywallController,
didPerform action: AdaptyUI.Action) {
switch action {
case let .custom(id):
if id == "openNewPaywall" {
// Display another paywall
}
}
break
}
}
```
---
# File: ios-handling-events
---
---
title: "Handle paywall events in iOS SDK"
description: "Handle subscription-related events in iOS using Adapty for better app monetization."
---
:::important
This guide covers event handling for purchases, restorations, product selection, and paywall rendering. You must also implement button handling (closing paywall, opening links, etc.). See our [guide on handling button actions](https://adapty.io/docs/handle-paywall-actions) for details.
:::
Paywalls configured with the [Paywall Builder](adapty-paywall-builder) don't need extra code to make and restore purchases. However, they generate some events that your app can respond to. Those events include button presses (close buttons, URLs, product selections, and so on) as well as notifications on purchase-related actions taken on the paywall. Learn how to respond to these events below.
This guide is for **new Paywall Builder paywalls** only which require Adapty SDK v3.0 or later. For presenting paywalls in Adapty SDK v2 designed with legacy Paywall Builder, see [iOS - Handle paywall events designed with legacy Paywall Builder](ios-handling-events-legacy).
:::tip
Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality.
:::
## Handling events in SwiftUI
To control or monitor processes occurring on the paywall screen within your mobile app, use the `.paywall` modifier in SwiftUI:
```swift showLineNumbers title="Swift"
@State var paywallPresented = false
var body: some View {
Text("Hello, AdaptyUI!")
.paywall(
isPresented: $paywallPresented,
paywall: paywall,
viewConfiguration: viewConfig,
didPerformAction: { action in
switch action {
case .close:
paywallPresented = false
case let .openURL(url):
// handle opening the URL (incl. for terms and privacy)
default:
// handle other actions
}
},
didSelectProduct: { /* Handle the event */ },
didStartPurchase: { /* Handle the event */ },
didFinishPurchase: { product, info in /* Handle the event */ },
didFailPurchase: { product, error in /* Handle the event */ },
didStartRestore: { /* Handle the event */ },
didFinishRestore: { /* Handle the event */ },
didFailRestore: { /* Handle the event */ },
didFailRendering: { error in
paywallPresented = false
},
didFailLoadingProducts: { error in
return false
}
)
}
```
You can register only the closure parameters you need, and omit those you do not need. In this case, unused closure parameters will not be created.
| Parameter | Required | Description |
|:----------------------------------|:---------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **isPresented** | required | A binding that manages whether the paywall screen is displayed. |
| **paywallConfiguration** | required | An `AdaptyUI.PaywallConfiguration` object containing visual details of the paywall. Use the `AdaptyUI.paywallConfiguration(for:products:viewConfiguration:observerModeResolver:tagResolver:timerResolver:)` method. Refer to [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls) topic for more details. |
| **didFailPurchase** | required | Invoked when a purchase fails due to errors (e.g., payment not allowed, network issues, invalid product). Not invoked for user cancellations or pending payments. |
| **didFinishRestore** | required | Invoked when purchase completes successfully. |
| **didFailRestore** | required | Invoked when restoring a purchase fails. |
| **didFailRendering** | required | Invoked if an error occurs while rendering the interface. In this case, [contact Adapty Support](mailto:support@adapty.io). |
| **fullScreen** | optional | Determines if the paywall appears in full-screen mode or as a modal. Defaults to `true`. |
| **didAppear** | optional | Invoked when the paywall view appears on screen. Also invoked when a user taps the [web paywall button](web-paywall#step-2a-add-a-web-purchase-button) inside a paywall, and a web paywall opens in an in-app browser. |
| **didDisappear** | optional | Invoked when the paywall view was dismissed. Also invoked when a [web paywall](web-paywall#step-2a-add-a-web-purchase-button) opened from a paywall in an in-app browser disappears from the screen. |
| **didPerformAction** | optional | Invoked when a user clicks a button. Different buttons have different action IDs. Two action IDs are pre-defined: `close` and `openURL`, while others are custom and can be set in the builder. |
| **didSelectProduct** | optional | If the product was selected for purchase (by a user or by the system), this callback will be invoked. |
| **didStartPurchase** | optional | Invoked when the user begins the purchase process. |
| **didFinishPurchase** | optional | Invoked when purchase completes successfully. |
| **didFinishWebPaymentNavigation** | optional | Invoked after attempting to open a [web paywall](web-paywall) for purchase, whether successful or failed. |
| **didStartRestore** | optional | Invoked when the user starts the restore process. |
| **didFailLoadingProducts** | optional | Invoked when errors occur during product loading. Return `true` to retry loading. |
| **didPartiallyLoadProducts** | optional | Invoked when products are partially loaded. |
| **showAlertItem** | optional | A binding that manages the display of alert items above the paywall. |
| **showAlertBuilder** | optional | A function for rendering the alert view. |
| **placeholderBuilder** | optional | A function for rendering the placeholder view while the paywall is loading. |
## Handling events in UIKit
To control or monitor processes occurring on the paywall screen within your mobile app, implement the `AdaptyPaywallControllerDelegate` methods.
### User-generated events
#### Product selection
If a user selects a product for purchase, this method will be invoked:
```swift showLineNumbers title="Swift"
func paywallController(
_ controller: AdaptyPaywallController,
didSelectProduct product: AdaptyPaywallProductWithoutDeterminingOffer
) { }
```
Event example (Click to expand)
```javascript
{
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
}
}
```
#### Started purchase
If a user initiates the purchase process, this method will be invoked:
```swift showLineNumbers title="Swift"
func paywallController(_ controller: AdaptyPaywallController,
didStartPurchase product: AdaptyPaywallProduct) {
}
```
Event example (Click to expand)
```javascript
{
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
}
}
```
It will not be invoked in Observer mode. Refer to the [iOS - Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode) topic for details.
#### Started purchase using a web paywall
If a user initiates the purchase process using a [web paywall](web-paywall.md), this method will be invoked:
```swift showLineNumbers title="Swift"
func paywallController(
_ controller: AdaptyPaywallController,
shouldContinueWebPaymentNavigation product: AdaptyPaywallProduct
) {
}
```
Event example (Click to expand)
```javascript
{
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
}
}
```
#### Successful or canceled purchase
If purchase succeeds, this method will be invoked:
```swift showLineNumbers title="Swift"
func paywallController(
_ controller: AdaptyPaywallController,
didFinishPurchase product: AdaptyPaywallProductWithoutDeterminingOffer,
purchaseResult: AdaptyPurchaseResult
) { }
}
```
Event examples (Click to expand)
```javascript
// Successful purchase
{
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
},
"purchaseResult": {
"type": "success",
"profile": {
"accessLevels": {
"premium": {
"id": "premium",
"isActive": true,
"expiresAt": "2024-02-15T10:30:00Z"
}
}
}
}
}
// Cancelled purchase
{
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
},
"purchaseResult": {
"type": "cancelled"
}
}
```
We recommend dismissing the paywall screen in that case.
It will not be invoked in Observer mode. Refer to the [iOS - Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode) topic for details.
#### Failed purchase
If a purchase fails due to an error, this method will be invoked. This includes StoreKit errors (payment restrictions, invalid products, network failures), transaction verification failures, and system errors. Note that user cancellations trigger `didFinishPurchase` with a cancelled result instead, and pending payments do not trigger this method.
```swift showLineNumbers title="Swift"
func paywallController(
_ controller: AdaptyPaywallController,
didFailPurchase product: AdaptyPaywallProduct,
error: AdaptyError
) { }
```
Event example (Click to expand)
```javascript
{
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
},
"error": {
"code": "purchase_failed",
"message": "Purchase failed due to insufficient funds",
"details": {
"underlyingError": "Insufficient funds in account"
}
}
}
```
It will not be invoked in Observer mode. Refer to the [iOS - Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode) topic for details.
#### Failed purchase using a web paywall
If `Adapty.openWebPaywall()` fails, this method will be invoked:
```swift showLineNumbers title="Swift"
func paywallController(
_ controller: AdaptyPaywallController,
didFailWebPaymentNavigation product: AdaptyPaywallProduct,
error: AdaptyError
) { }
```
Event example (Click to expand)
```javascript
{
"product": {
"vendorProductId": "premium_monthly",
"localizedTitle": "Premium Monthly",
"localizedDescription": "Premium subscription for 1 month",
"localizedPrice": "$9.99",
"price": 9.99,
"currencyCode": "USD"
},
"error": {
"code": "web_payment_failed",
"message": "Web payment navigation failed",
"details": {
"underlyingError": "Network connection error"
}
}
}
```
#### Successful restore
If restoring a purchase succeeds, this method will be invoked:
```swift showLineNumbers title="Swift"
func paywallController(
_ controller: AdaptyPaywallController,
didFinishRestoreWith profile: AdaptyProfile
) { }
```
Event example (Click to expand)
```javascript
{
"profile": {
"accessLevels": {
"premium": {
"id": "premium",
"isActive": true,
"expiresAt": "2024-02-15T10:30:00Z"
}
},
"subscriptions": [
{
"vendorProductId": "premium_monthly",
"isActive": true,
"expiresAt": "2024-02-15T10:30:00Z"
}
]
}
}
```
We recommend dismissing the screen if a the has the required `accessLevel`. Refer to the [Subscription status](subscription-status) topic to learn how to check it.
#### Failed restore
If restoring a purchase fails, this method will be invoked:
```swift showLineNumbers title="Swift"
public func paywallController(
_ controller: AdaptyPaywallController,
didFailRestoreWith error: AdaptyError
) { }
```
Event example (Click to expand)
```javascript
{
"error": {
"code": "restore_failed",
"message": "Purchase restoration failed",
"details": {
"underlyingError": "No previous purchases found"
}
}
}
```
### Data fetching and rendering
#### Product loading errors
If you don't pass the product array during the initialization, AdaptyUI will retrieve the necessary objects from the server by itself. If this operation fails, AdaptyUI will report the error by calling this method:
```swift showLineNumbers title="Swift"
public func paywallController(
_ controller: AdaptyPaywallController,
didFailLoadingProductsWith error: AdaptyError
) -> Bool {
return true
}
```
Event example (Click to expand)
```javascript
{
"error": {
"code": "products_loading_failed",
"message": "Failed to load products from the server",
"details": {
"underlyingError": "Network timeout"
}
}
}
```
If you return `true`, AdaptyUI will repeat the request after 2 seconds.
#### Rendering errors
If an error occurs during the interface rendering, it will be reported by this method:
```swift showLineNumbers title="Swift"
public func paywallController(
_ controller: AdaptyPaywallController,
didFailRenderingWith error: AdaptyError
) { }
```
Event example (Click to expand)
```javascript
{
"error": {
"code": "rendering_failed",
"message": "Failed to render paywall interface",
"details": {
"underlyingError": "Invalid paywall configuration"
}
}
}
```
In a normal situation, such errors should not occur, so if you come across one, please let us know.
---
# File: ios-use-fallback-paywalls
---
---
title: "iOS - Use fallback paywalls"
description: "Handle cases when users are offline or Adapty servers aren't available"
---
:::warning
Fallback paywalls are supported by iOS SDK v2.11 or later.
:::
To maintain a fluid user experience, it is important to set up [fallbacks](/fallback-paywalls) for your [paywalls](paywalls) and [onboardings](onboardings). This precaution extends the application's capabilities in case of partial or complete loss of internet connection.
* **If the application cannot access Adapty servers:**
It will be able to display a fallback paywall, and access the local onboarding configuration.
* **If the application cannot access the internet:**
It will be able to display a fallback paywall. Onboardings include remote content and require an internet connection to function.
:::important
Before you follow the steps in this guide, [download](/local-fallback-paywalls) the fallback configuration files from Adapty.
:::
## Configuration
1. Add the fallback JSON file to your project bundle: open the **File** menu in XCode and select the **Add Files to "YourProjectName"** option.
2. Call the `.setFallback` method **before** you fetch the target paywall or onboarding.
```swift showLineNumbers
do {
if let urlPath = Bundle.main.url(forResource: fileName, withExtension: "json") {
try await Adapty.setFallback(fileURL: urlPath)
}
} catch {
// handle the error
}
```
```swift showLineNumbers
if let url = Bundle.main.url(forResource: "ios_fallback", withExtension: "json") {
Adapty.setFallback(fileURL: url)
}
```
Parameters:
| Parameter | Description |
| :---------- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **fileURL** | Path to the fallback configuration file. |
---
# File: localizations-and-locale-codes
---
---
title: "Use localizations and locale codes in iOS SDK"
description: "Manage app localizations and locale codes to reach a global audience in your iOS app."
---
## Why this is important
There are a few scenarios when locale codes come into play — for example, when you're trying to fetch the correct paywall for the current localization of your app.
As locale codes are complicated and can vary from platform to platform, we rely on an internal standard for all the platforms we support. However, because these codes are complicated, it is really important for you to understand what exactly are you sending to our server to get the correct localization, and what happens next — so you will always receive what you expect.
## Locale code standard at Adapty
For locale codes, Adapty uses a slightly modified [BCP 47 standard](https://en.wikipedia.org/wiki/IETF_language_tag): every code consists of lowercase subtags, separated by hyphens. Some examples: `en` (English), `pt-br` (Portuguese (Brazil)), `zh` (Simplified Chinese), `zh-hant` (Traditional Chinese).
## Locale code matching
When Adapty receives a call from the client-side SDK with the locale code and starts looking for a corresponding localization of a paywall, the following happens:
1. The incoming locale string is converted to lowercase and all the underscores (`_`) are replaced with hyphens (`-`)
2. We then look for the localization with the fully matching locale code
3. If no match was found, we take the substring before the first hyphen (`pt` for `pt-br`) and look for the matching localization
4. If no match was found again, we return the default `en` localization
This way an iOS device that sent `'pt_BR'`, an Android device that sent `pt-BR`, and another device that sent `pt-br` will get the same result.
## Implementing localizations: recommended way
If you're wondering about localizations, chances are you're already dealing with the localized string files in your project. If that's the case, we recommend placing some key-value with the intended Adapty locale code in each of your files for the corresponding localizations. And then extract the value for this key when calling our SDK, like so:
```swift showLineNumbers
// 1. Modify your Localizable.strings files
/*
Localizable.strings - Spanish
*/
adapty_paywalls_locale = "es";
/*
Localizable.strings - Portuguese (Brazil)
*/
adapty_paywalls_locale = "pt-br";
// 2. Extract and use the locale code
let locale = NSLocalizedString("adapty_paywalls_locale", comment: "")
// pass locale code to AdaptyUI.getViewConfiguration or Adapty.getPaywall method
```
That way you can ensure you're in full control of what localization will be retrieved for every user of your app.
## Implementing localizations: the other way
You can get similar (but not identical) results without explicitly defining locale codes for every localization. That would mean extracting a locale code from some other objects that your platform provides, like this:
```swift showLineNumbers
let locale = Locale.current.identifier
// pass locale code to AdaptyUI.getViewConfiguration or Adapty.getPaywall method
```
Note that we don't recommend this approach due to few reasons:
1. On iOS preferred languages and current locale are not identical. If you want the localization to be picked correctly you'll have to either rely on Apple's logic, which works out of the box if you're using the recommended approach with localized string files, or re-create it.
2. It's hard to predict what exactly will Adapty's server get. For example, on iOS, it is possible to obtain a locale like `ar_OM@numbers='latn'` on a device and send it to our server. And for this call you will get not the `ar-om` localization you were looking for, but rather `ar`, which is likely unexpected.
Should you decide to use this approach anyway — make sure you've covered all the relevant use cases.
---
# File: ios-web-paywall
---
---
title: "Implement web paywalls in iOS SDK"
description: "Set up a web paywall to get paid without the App Store fees and audits."
---
:::important
Before you begin, make sure you have [configured your web paywall in the dashboard](web-paywall.md) and installed Adapty SDK version 3.6.1 or later.
:::
## Open web paywalls
If you are working with a paywall you developed yourself, you need to handle web paywalls using the SDK method. The `.openWebPaywall` method:
1. Generates a unique URL allowing Adapty to link a specific paywall shown to a particular user to the web page they are redirected to.
2. Tracks when your users return to the app and then requests `.getProfile` at short intervals to determine whether the profile access rights have been updated.
This way, if the payment has been successful and access rights have been updated, the subscription activates in the app almost immediately.
```swift showLineNumbers title="Swift"
do {
try await Adapty.openWebPaywall(for: product)
} catch {
print("Failed to open web paywall: \(error)")
}
```
:::note
There are two versions of the `openWebPaywall` method:
1. `openWebPaywall(product)` that generates URLs by paywall and adds the product data to URLs as well.
2. `openWebPaywall(paywall)` that generates URLs by paywall without adding the product data to URLs. Use it when your products in the Adapty paywall differ from those in the web paywall.
:::
## Handle errors
| Error | Description | Recommended action |
|-----------------------------------------|--------------------------------------------------------|---------------------------------------------------------------------------|
| AdaptyError.paywallWithoutPurchaseUrl | The paywall doesn't have a web purchase URL configured | Check if the paywall has been properly configured in the Adapty Dashboard |
| AdaptyError.productWithoutPurchaseUrl | The product doesn't have a web purchase URL | Verify the product configuration in the Adapty Dashboard |
| AdaptyError.failedOpeningWebPaywallUrl | Failed to open the URL in the browser | Check device settings or provide an alternative purchase method |
| AdaptyError.failedDecodingWebPaywallUrl | Failed to properly encode parameters in the URL | Verify URL parameters are valid and properly formatted |
## Implementation example
```swift showLineNumbers title="Swift"
class SubscriptionViewController: UIViewController {
var paywall: AdaptyPaywall?
@IBAction func purchaseButtonTapped(_ sender: UIButton) {
guard let paywall = paywall, let product = paywall.products.first else { return }
Task {
await offerWebPurchase(for: product)
}
}
func offerWebPurchase(for paywallProduct: AdaptyPaywallProduct) async {
do {
// Attempt to open web paywall
try await Adapty.openWebPaywall(for: paywallProduct)
} catch let error as AdaptyError {
switch error {
case .paywallWithoutPurchaseUrl, .productWithoutPurchaseUrl:
showAlert(message: "Web purchase is not available for this product.")
case .failedOpeningWebPaywallUrl:
showAlert(message: "Could not open web browser. Please try again.")
default:
showAlert(message: "An error occurred: \(error.localizedDescription)")
}
} catch {
showAlert(message: "An unexpected error occurred.")
}
}
// Helper methods
private func showAlert(message: String) { /* ... */ }
}
```
:::note
After users return to the app, refresh the UI to reflect the profile updates. `AdaptyDelegate` will receive and process profile update events.
:::
## Open web paywalls in an in-app browser
:::important
Opening web paywalls in an in-app browser is supported starting from Adapty SDK v. 3.15.
:::
By default, web paywalls open in the external browser.
To provide a seamless user experience, you can open web paywalls in an in-app browser. This displays the web purchase page within your application, allowing users to complete transactions without switching apps.
To enable this, set the `in` parameter to `.inAppBrowser`:
```swift showLineNumbers title="Swift"
do {
try await Adapty.openWebPaywall(for: product, in: .inAppBrowser) // default – .externalBrowser
} catch {
print("Failed to open web paywall: \(error)")
}
```
---
# File: ios-troubleshoot-paywall-builder
---
---
title: "Troubleshoot Paywall Builder in iOS SDK"
description: "Troubleshoot Paywall Builder in iOS SDK"
---
This guide helps you resolve common issues when using paywalls designed in the Adapty Paywall Builder in the iOS SDK.
## Getting a paywall configuration fails
**Issue**: The `getPaywallConfiguration` method fails to retrieve paywall configuration.
**Reason**: The paywall is not enabled for device display in the Paywall Builder.
**Solution**: Enable the **Show on device** toggle in the Paywall Builder.
## The paywall view number is too big
**Issue**: The paywall view count is showing double the expected number.
**Reason**: You may be calling `logShowPaywall` in your code, which duplicates the view count if you're using the Paywall builder. For paywalls designed with the Paywall Builder, analytics are tracked automatically, so you don't need to use this method.
**Solution**: Ensure you are not calling `logShowPaywall` in your code if you're using the Paywall builder.
## Other issues
**Issue**: You're experiencing other Paywall Builder-related problems not covered above.
**Solution**: Migrate the SDK to the latest version using the [migration guides](ios-sdk-migration-guides) if needed. Many issues are resolved in newer SDK versions.
---
# File: ios-quickstart-manual
---
---
title: "Enable purchases in your custom paywall in iOS SDK"
description: "Integrate Adapty SDK into your custom iOS paywalls to enable in-app purchases."
---
This guide describes how to integrate Adapty into your custom paywalls. Keep full control over paywall implementation, while the Adapty SDK fetches products, handles new purchases, and restores previous ones.
:::important
**This guide is for developers who are implementing custom paywalls.** If you want the easiest way to enable purchases, use the [Adapty Paywall Builder](ios-quickstart-paywalls.md). With Paywall Builder, you create paywalls in a no-code visual editor, Adapty handles all purchase logic automatically, and you can test different designs without republishing your app.
:::
## Before you start
### Set up products
To enable in-app purchases, you need to understand three key concepts:
- [**Products**](product.md) – anything users can buy (subscriptions, consumables, lifetime access)
- [**Paywalls**](paywalls.md) – configurations that define which products to offer. In Adapty, paywalls are the only way to retrieve products, but this design lets you modify products, prices, and offers without touching your app code.
- [**Placements**](placements.md) – where and when you show paywalls in your app (like `main`, `onboarding`, `settings`). You set up paywalls for placements in the dashboard, then request them by placement ID in your code. This makes it easy to run A/B tests and show different paywalls to different users.
Make sure you understand these concepts even if you work with your custom paywall. Basically, they are just your way to manage the products you sell in your app.
To implement your custom paywall, you will need to create a **paywall** and add it to a **placement**. This setup allows you to retrieve your products. To understand what you need to do in the dashboard, follow the quickstart guide [here](quickstart.md).
### Manage users
You can work either with or without backend authentication on your side.
However, the Adapty SDK handles anonymous and identified users differently. Read the [identification quickstart guide](ios-quickstart-identify.md) to understand the specifics and ensure you are working with users properly.
## Step 1. Get products
To retrieve products for your custom paywall, you need to:
1. Get the `paywall` object by passing [placement](placements.md) ID to the `getPaywall` method.
2. Get the products array for this paywall using the `getPaywallProducts` method.
```swift
func loadPaywall() async {
do {
let paywall = try await Adapty.getPaywall("YOUR_PLACEMENT_ID")
let products = try await Adapty.getPaywallProducts(paywall: paywall)
// Use products to build your custom paywall UI
} catch {
// Handle the error
}
}
```
```swift
func loadPaywall() {
Adapty.getPaywall("YOUR_PLACEMENT_ID") { result in
switch result {
case let .success(paywall):
Adapty.getPaywallProducts(paywall: paywall) { result in
switch result {
case let .success(products):
// Use products to build your custom paywall UI
case let .failure(error):
// Handle the error
}
}
case let .failure(error):
// Handle the error
}
}
}
```
## Step 2. Accept purchases
When a user taps on a product in your custom paywall, call the `makePurchase` method with the selected product. This will handle the purchase flow and return the updated profile.
```swift
func purchaseProduct(_ product: AdaptyPaywallProduct) async {
do {
let purchaseResult = try await Adapty.makePurchase(product: product)
switch purchaseResult {
case .userCancelled:
// User canceled the purchase
break
case .pending:
// Purchase is pending (e.g., awaiting parental approval)
break
case let .success(profile, transaction):
// Purchase successful, profile updated
break
}
} catch {
// Handle the error
}
}
```
```swift
func purchaseProduct(_ product: AdaptyPaywallProduct) {
Adapty.makePurchase(product: product) { result in
switch result {
case let .success(purchaseResult):
switch purchaseResult {
case .userCancelled:
// User canceled the purchase
break
case .pending:
// Purchase is pending (e.g., awaiting parental approval)
break
case let .success(profile, transaction):
// Purchase successful, profile updated
break
}
case let .failure(error):
// Handle the error
}
}
}
```
## Step 3. Restore purchases
Apple requires all apps with subscriptions to provide a way users can restore their purchases. While purchases are automatically restored when a user logs in with their Apple ID, you must still implement a restore button in your app.
Call the `restorePurchases` method when the user taps the restore button. This will sync their purchase history with Adapty and return the updated profile.
```swift
func restorePurchases() async {
do {
let profile = try await Adapty.restorePurchases()
// Restore successful, profile updated
} catch {
// Handle the error
}
}
```
```swift
func restorePurchases() {
Adapty.restorePurchases { result in
switch result {
case let .success(profile):
// Restore successful, profile updated
case let .failure(error):
// Handle the error
}
}
}
```
## Next steps
Your paywall is ready to be displayed in the app. [Test your purchases in sandbox mode](test-purchases-in-sandbox) to make sure you can complete a test purchase from the paywall.
Next, [check whether users have completed their purchase](ios-check-subscription-status.md) to determine whether to display the paywall or grant access to paid features.
---
# File: fetch-paywalls-and-products
---
---
title: "Fetch paywalls and products for remote config paywalls in iOS SDK"
description: "Fetch paywalls and products in Adapty iOS SDK to enhance user monetization."
---
Before showcasing remote config and custom paywalls, you need to fetch the information about them. Please be aware that this topic refers to remote config and custom paywalls. For guidance on fetching paywalls for Paywall Builder-customized paywalls, please consult the [iOS](get-pb-paywalls.md), [Android](android-get-pb-paywalls.md), [React Native](react-native-get-pb-paywalls.md), [Flutter](flutter-get-pb-paywalls.md), and [Unity](unity-get-pb-paywalls.md).
:::tip
Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality.
:::
Before you start fetching paywalls and products in your mobile app (click to expand)
1. [Create your products](create-product) in the Adapty Dashboard.
2. [Create a paywall and incorporate the products into your paywall](create-paywall) in the Adapty Dashboard.
3. [Create placements and incorporate your paywall into the placement](create-placement) in the Adapty Dashboard.
4. [Install Adapty SDK](sdk-installation-ios) in your mobile app.
## Fetch paywall information
In Adapty, a [product](product) serves as a combination of products from both the App Store and Google Play. These cross-platform products are integrated into paywalls, enabling you to showcase them within specific mobile app placements.
To display the products, you need to obtain a [Paywall](paywalls) from one of your [placements](placements) with `getPaywall` method.
:::important
**Don't hardcode product IDs.** The only ID you should hardcode is the placement ID. Paywalls are configured remotely, so the number of products and available offers can change at any time. Your app must handle these changes dynamically—if a paywall returns two products today and three tomorrow, display all of them without code changes.
:::
```swift showLineNumbers
do {
let paywall = try await Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID")
// the requested paywall
} catch {
// handle the error
}
```
```swift showLineNumbers
Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en") { result in
switch result {
case let .success(paywall):
// the requested paywall
case let .failure(error):
// handle the error
}
}
```
| Parameter | Presence | Description |
|---------|--------|-----------|
| **placementId** | required | The identifier of the [Placement](placements). This is the value you specified when creating a placement in your Adapty Dashboard. |
| **locale** |
optional
default: `en`
|
The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores paywalls in two layers: regularly updated cache described above and [fallback paywalls](fallback-paywalls) . We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.
|
| **loadTimeout** | default: 5 sec |
This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.
Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.
|
Don't hardcode product IDs! Since paywalls are configured remotely, the available products, the number of products, and special offers (such as free trials) can change over time. Make sure your code handles these scenarios.
For example, if you initially retrieve 2 products, your app should display those 2 products. However, if you later retrieve 3 products, your app should display all 3 without requiring any code changes. The only thing you have to hardcode is placement ID.
Response parameters:
| Parameter | Description |
| :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Paywall | An [`AdaptyPaywall`](https://swift.adapty.io/documentation/adapty/adaptypaywall) object with: a list of product IDs, the paywall identifier, remote config, and several other properties. |
## Fetch products
Once you have the paywall, you can query the product array that corresponds to it:
```swift showLineNumbers
do {
let products = try await Adapty.getPaywallProducts(paywall: paywall)
// the requested products array
} catch {
// handle the error
}
```
```swift showLineNumbers
Adapty.getPaywallProducts(paywall: paywall) { result in
switch result {
case let .success(products):
// the requested products array
case let .failure(error):
// handle the error
}
}
```
Response parameters:
| Parameter | Description |
| :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Products | List of [`AdaptyPaywallProduct`](https://swift.adapty.io/documentation/adapty/adaptypaywallproduct) objects with: product identifier, product name, price, currency, subscription length, and several other properties. |
When implementing your own paywall design, you will likely need access to these properties from the [`AdaptyPaywallProduct`](https://swift.adapty.io/documentation/adapty/adaptypaywallproduct) object. Illustrated below are the most commonly used properties, but refer to the linked document for full details on all available properties.
| Property | Description |
|-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **Title** | To display the title of the product, use `product.localizedTitle`. Note that the localization is based on the users' selected store country rather than the locale of the device itself. |
| **Price** | To display a localized version of the price, use `product.localizedPrice`. This localization is based on the locale info of the device. You can also access the price as a number using `product.price`. The value will be provided in the local currency. To get the associated currency symbol, use `product.currencySymbol`. |
| **Subscription Period** | To display the period (e.g. week, month, year, etc.), use `product.localizedSubscriptionPeriod`. This localization is based on the locale of the device. To fetch the subscription period programmatically, use `product.subscriptionPeriod`. From there you can access the `unit` enum to get the length (i.e. day, week, month, year, or unknown). The `numberOfUnits` value will get you the number of period units. For example, for a quarterly subscription, you'd see `.month` in the unit property, and `3` in the numberOfUnits property. |
| **Introductory Offer** | To display a badge or other indicator that a subscription contains an introductory offer, check out the `product.subscriptionOffer` property. Within this object are the following helpful properties: • `offerType`: an enum with values `introductory`, `promotional`, and `winBack`. Free trials and initial discounted subscriptions will be the `introductory` type. • `price`: The discounted price as a number. For free trials, look for `0` here. • `localizedPrice`: A formatted price of the discount for the user's locale. • `localizedNumberOfPeriods`: a string localized using the device's locale describing the length of the offer. For example, a three day trial offer shows `3 days` in this field. • `subscriptionPeriod`: Alternatively, you can get the individual details of the offer period with this property. It works in the same manner for offers as the previous section describes. • `localizedSubscriptionPeriod`: A formatted subscription period of the discount for the user's locale. |
## Check intro offer eligibility on iOS
By default, the `getPaywallProducts` method checks eligibility for introductory, promotional, and win-back offers. If you need to display products before the SDK determines offer eligibility, use the `getPaywallProductsWithoutDeterminingOffer` method instead.
:::note
After showing the initial products, be sure to call the regular `getPaywallProducts` method to update the products with accurate offer eligibility information.
:::
```swift showLineNumbers
do {
let products = try await Adapty.getPaywallProductsWithoutDeterminingOffer(paywall: paywall)
// the requested products array without subscriptionOffer
} catch {
// handle the error
}
```
```swift showLineNumbers
Adapty.getPaywallProductsWithoutDeterminingOffer(paywall: paywall) { result in
switch result {
case let .success(products):
// the requested products array without subscriptionOffer
case let .failure(error):
// handle the error
}
}
```
## Speed up paywall fetching with default audience paywall
Typically, paywalls are fetched almost instantly, so you don’t need to worry about speeding up this process. However, in cases where you have numerous audiences and paywalls, and your users have a weak internet connection, fetching a paywall may take longer than you'd like. In such situations, you might want to display a default paywall to ensure a smooth user experience rather than showing no paywall at all.
To address this, you can use the `getPaywallForDefaultAudience` method, which fetches the paywall of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the paywall by the `getPaywall` method, as detailed in the [Fetch Paywall Information](fetch-paywalls-and-products#fetch-paywall-information) section above.
:::warning
Why we recommend using `getPaywall`
The `getPaywallForDefaultAudience` method comes with a few significant drawbacks:
- **Potential backward compatibility issues**: If you need to show different paywalls for different app versions (current and future), you may face challenges. You’ll either have to design paywalls that support the current (legacy) version or accept that users with the current (legacy) version might encounter issues with non-rendered paywalls.
- **Loss of targeting**: All users will see the same paywall designed for the **All Users** audience, which means you lose personalized targeting (including based on countries, marketing attribution or your own custom attributes).
If you're willing to accept these drawbacks to benefit from faster paywall fetching, use the `getPaywallForDefaultAudience` method as follows. Otherwise, stick to the `getPaywall` described [above](fetch-paywalls-and-products#fetch-paywall-information).
:::
```swift showLineNumbers
do {
let paywall = try await Adapty.getPaywallForDefaultAudience("YOUR_PLACEMENT_ID")
// the requested paywall
} catch {
// handle the error
}
```
```swift showLineNumbers
Adapty.getPaywallForDefaultAudience(placementId: "YOUR_PLACEMENT_ID", locale: "en") { result in
switch result {
case let .success(paywall):
// the requested paywall
case let .failure(error):
// handle the error
}
}
```
:::note
The `getPaywallForDefaultAudience` method is available starting from iOS SDK version 2.11.2.
:::
| Parameter | Presence | Description |
|---------|--------|-----------|
| **placementId** | required | The identifier of the [Placement](placements). This is the value you specified when creating a placement in your Adapty Dashboard. |
| **locale** |
optional
default: `en`
|
The identifier of the [paywall localization](add-remote-config-locale). This parameter is expected to be a language code composed of one or more subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
|
---
# File: present-remote-config-paywalls
---
---
title: "Render paywall designed by remote config in iOS SDK"
description: "Discover how to present remote config paywalls in Adapty to personalize user experience."
---
If you've customized a paywall using remote config, you'll need to implement rendering in your mobile app's code to display it to users. Since remote config offers flexibility tailored to your needs, you're in control of what's included and how your paywall view appears. We provide a method for fetching the remote configuration, giving you the autonomy to showcase your custom paywall configured via remote config.
Don't forget to [check if a user is eligible for an introductory offer in iOS](fetch-paywalls-and-products#check-intro-offer-eligibility-on-ios) and adjust the paywall view to process the case when they are eligible.
## Get paywall remote config and present it
To get a remote config of a paywall, access the `remoteConfig` property and extract the needed values.
```swift showLineNumbers
do {
let paywall = try await Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID")
let headerText = paywall.remoteConfig?.dictionary?["header_text"] as? String
} catch {
// handle the error
}
```
```swift showLineNumbers
Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID") { result in
let paywall = try? result.get()
let headerText = paywall?.remoteConfig?.dictionary?["header_text"] as? String
}
```
At this point, once you've received all the necessary values, it's time to render and assemble them into a visually appealing page. Ensure that the design accommodates various mobile phone screens and orientations, providing a seamless and user-friendly experience across different devices.
:::warning
Make sure to [record the paywall view event](present-remote-config-paywalls#track-paywall-view-events) as described below, allowing Adapty analytics to capture information for funnels and A/B tests.
:::
After you've done with displaying the paywall, continue with setting up a purchase flow. When the user makes a purchase, simply call `.makePurchase()` with the product from your paywall. For details on the`.makePurchase()` method, read [Making purchases](making-purchases).
We recommend [creating a backup paywall called a fallback paywall](fallback-paywalls). This backup will display to the user when there's no internet connection or cache available, ensuring a smooth experience even in these situations.
## Track paywall view events
Adapty assists you in measuring the performance of your paywalls. While we gather data on purchases automatically, logging paywall views needs your input because only you know when a customer sees a paywall.
To log a paywall view event, simply call `.logShowPaywall(paywall)`, and it will be reflected in your paywall metrics in funnels and A/B tests.
:::important
Calling `.logShowPaywall(paywall)` is not needed if you are displaying paywalls created in the [paywall builder](adapty-paywall-builder.md).
:::
```swift showLineNumbers
Adapty.logShowPaywall(paywall)
```
Request parameters:
| Parameter | Presence | Description |
| :---------- | :------- |:-----------------------------------------------------------------------------------------|
| **paywall** | required | An [`AdaptyPaywall`](https://swift.adapty.io/documentation/adapty/adaptypaywall) object. |
---
# File: making-purchases
---
---
title: "Make purchases in mobile app in iOS SDK"
description: "Guide on handling in-app purchases and subscriptions using Adapty."
---
Displaying paywalls within your mobile app is an essential step in offering users access to premium content or services. However, simply presenting these paywalls is enough to support purchases only if you use [Paywall Builder](adapty-paywall-builder) to customize your paywalls.
If you don't use the Paywall Builder, you must use a separate method called `.makePurchase()` to complete a purchase and unlock the desired content. This method serves as the gateway for users to engage with the paywalls and proceed with their desired transactions.
If your paywall has an active promotional offer for the product a user is trying to buy, Adapty will automatically apply it at the time of purchase.
:::warning
Keep in mind that the introductory offer will be applied automatically only if you use the paywalls set up using the Paywall Builder.
In other cases, you'll need to [verify the user's eligibility for an introductory offer on iOS](fetch-paywalls-and-products#check-intro-offer-eligibility-on-ios). Skipping this step may result in your app being rejected during release. Moreover, it could lead to charging the full price to users who are eligible for an introductory offer.
:::
Make sure you've [done the initial configuration](quickstart) without skipping a single step. Without it, we can't validate purchases.
## Make purchase
:::note
**Using [Paywall Builder](adapty-paywall-builder)?** Purchases are processed automatically—you can skip this step.
**Looking for step-by-step guidance?** Check out the [quickstart guide](ios-implement-paywalls-manually) for end-to-end implementation instructions with full context.
:::
```swift showLineNumbers
do {
let purchaseResult = try await Adapty.makePurchase(product: product)
switch purchaseResult {
case .userCancelled:
// Handle the case where the user canceled the purchase
case .pending:
// Handle deferred purchases (e.g., the user will pay offline with cash)
case let .success(profile, transaction):
if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false {
// Grant access to the paid features
}
}
} catch {
// Handle the error
}
```
```swift showLineNumbers
Adapty.makePurchase(product: product) { result in
switch result {
case let .success(purchaseResult):
switch purchaseResult {
case .userCancelled:
// Handle the case where the user canceled the purchase
case .pending:
// Handle deferred purchases (e.g., the user will pay offline with cash)
case let .success(profile, transaction):
if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false {
// Grant access to the paid features
}
}
case let .failure(error):
// Handle the error
}
}
```
Request parameters:
| Parameter | Presence | Description |
| :---------- | :------- | :-------------------------------------------------------------------------------------------------- |
| **Product** | required | An [`AdaptyPaywallProduct`](https://swift.adapty.io/documentation/adapty/adaptypaywallproduct) object retrieved from the paywall. |
Response parameters:
| Parameter | Description |
|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **Profile** |
If the request has been successful, the response contains this object. An [AdaptyProfile](https://swift.adapty.io/documentation/adapty/adaptyprofile) object provides comprehensive information about a user's access levels, subscriptions, and non-subscription purchases within the app.
Check the access level status to ascertain whether the user has the required access to the app.
|
:::warning
**Note:** if you're still on Apple's StoreKit version lower than v2.0 and Adapty SDK version lower than v.2.9.0, you need to provide [Apple App Store shared secret](app-store-connection-configuration#step-4-enter-app-store-shared-secret) instead. This method is currently deprecated by Apple.
:::
## In-app purchases from the App Store
When a user initiates a purchase in the App Store and the transaction carries over to your app, you have two options:
- **Process the transaction immediately:** Return `true` in `shouldAddStorePayment`. This will trigger the Apple purchase system screen right away.
- **Store the product object for later processing:** Return `false` in `shouldAddStorePayment`, then call `makePurchase` with the stored product later. This may be useful if you need to show something custom to your user before triggering a purchase.
Here’s the complete snippet:
```swift showLineNumbers title="Swift"
final class YourAdaptyDelegateImplementation: AdaptyDelegate {
nonisolated func shouldAddStorePayment(for product: AdaptyDeferredProduct) -> Bool {
// 1a.
// Return `true` to continue the transaction in your app. The Apple purchase system screen will show automatically.
// 1b.
// Store the product object and return `false` to defer or cancel the transaction.
false
}
// 2. Continue the deferred purchase later on by passing the product to `makePurchase` when the timing is appropriate
func continueDeferredPurchase() async {
let storedProduct: AdaptyDeferredProduct = // get the product object from 1b.
do {
try await Adapty.makePurchase(product: storedProduct)
} catch {
// handle the error
}
}
}
```
## Redeem Offer Code in iOS
Since iOS 14.0, your users can redeem Offer Codes. Code redemption means using a special code, like a promotional or gift card code, to get free access to content or features in an app or on the App Store. To enable users to redeem offer codes, you can display the offer code redemption sheet by using the appropriate SDK method:
```swift showLineNumbers
Adapty.presentCodeRedemptionSheet()
```
:::danger
Based on our observations, the Offer Code Redemption sheet in some apps may not work reliably. We recommend redirecting the user directly to the App Store.
In order to do this, you need to open the url of the following format:
`https://apps.apple.com/redeem?ctx=offercodes&id={apple_app_id}&code={code}`
:::
---
# File: restore-purchase
---
---
title: "Restore purchases in mobile app in iOS SDK"
description: "Learn how to restore purchases in Adapty to ensure seamless user experience."
---
Restoring Purchases is a feature that allows users to regain access to previously purchased content, such as subscriptions or in-app purchases, without being charged again. This feature is especially useful for users who may have uninstalled and reinstalled the app or switched to a new device and want to access their previously purchased content without paying again.
:::note
In paywalls built with [Paywall Builder](adapty-paywall-builder), purchases are restored automatically without additional code from you. If that's your case — you can skip this step.
:::
To restore a purchase if you do not use the [Paywall Builder](adapty-paywall-builder) to customize the paywall, call `.restorePurchases()` method:
```swift showLineNumbers
do {
let profile = try await Adapty.restorePurchases()
if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false {
// successful access restore
}
} catch {
// handle the error
}
```
```swift showLineNumbers
Adapty.restorePurchases { [weak self] result in
switch result {
case let .success(profile):
if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false {
// successful access restore
}
case let .failure(error):
// handle the error
}
}
```
Response parameters:
| Parameter | Description |
|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **Profile** |
An [`AdaptyProfile`](https://swift.adapty.io/documentation/adapty/adaptyprofile) object. This model contains info about access levels, subscriptions, and non-subscription purchases.
Сheck the **access level status** to determine whether the user has access to the app.
|
:::tip
Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality.
:::
---
# File: ios-transaction-management
---
---
title: "Advanced transaction management in iOS SDK"
description: "Finish transactions manually in your iOS app with Adapty SDK."
---
:::note
Advanced transaction management is supported in the Adapty iOS SDK starting from version 3.12.
:::
Advanced transaction management in Adapty gives you more control over how transactions are handled, verified, and finished.
Advanced transaction management introduces three optional features that work together:
| Feature | Purpose |
|-------------------------------------------------------------|----------|
| [`appAccountToken`](#assign-appaccounttoken) | Links Apple transactions to your internal user ID |
| [`jwsTransaction`](#access-the-jws-representation) | Provides Apple’s signed transaction payload for validation |
| [Manual finishing](#control-transaction-finishing-behavior) | Lets you finish transactions only after your backend confirms success |
Together, these tools let you build robust custom validation flows while Adapty continues syncing transactions with its backend.
:::important
Most apps don’t need this.
By default, Adapty automatically validates and finishes StoreKit transactions.
Use this guide only if you run your own backend validation or want to fully control the purchase lifecycle.
:::
## Assign `appAccountToken`
[`appAccountToken`](https://developer.apple.com/documentation/storekit/product/purchaseoption/appaccounttoken(_:)) is a **UUID** that lets you link App Store transactions to your internal user identity.
StoreKit associates this token with every transaction, so your backend can match App Store data to your users.
Use a stable UUID generated per user and reuse it for the same account across devices.
This ensures that purchases and App Store notifications stay correctly linked.
You can set the token in two ways – during the SDK activation or when identifying the user.
:::important
You must always pass `appAccountToken` together with `customerUserId`.
If you pass only the token, it will not be included in the transaction.
:::
```swift showLineNumbers
// During configuration:
let configurationBuilder =
AdaptyConfiguration
.builder(withAPIKey: "PUBLIC_SDK_KEY")
.with(customerUserId: "YOUR_USER_ID", withAppAccountToken: UUID())
do {
try await Adapty.activate(with: configurationBuilder.build())
} catch {
// handle the error
}
// Or when identifying a user:
do {
try await Adapty.identify("YOUR_USER_ID", withAppAccountToken: UUID())
} catch {
// handle the error
}
```
```swift showLineNumbers
// During configuration:
let configurationBuilder =
AdaptyConfiguration
.builder(withAPIKey: "PUBLIC_SDK_KEY")
.with(customerUserId: "YOUR_USER_ID", withAppAccountToken: )
Adapty.activate(with: configurationBuilder.build()) { error in
// handle the error
}
// Or when identifying a user:
Adapty.identify("YOUR_USER_ID", withAppAccountToken: ) { error in
if let error {
// handle the error
}
}
```
## Access the JWS representation
When you make a purchase, the result includes Apple’s transaction in [JWS Compact Serialization format](https://developer.apple.com/documentation/storekit/verificationresult/jwsrepresentation-21vgo).
You can forward this value to your backend for independent validation or logging.
```swift
let result = try await Adapty.makePurchase(product: paywallProduct)
let jwsRepresentation = result.jwsTransaction
```
## Control transaction finishing behavior
By default, Adapty automatically finishes StoreKit transactions after validation.
If you need to delay finishing until your backend confirms success, set the finishing behavior to manual.
In this mode:
- Adapty still validates purchases and syncs them with its backend.
- Transactions remain unfinished until you explicitly call `finish()`.
```swift
var configBuilder = AdaptyConfiguration
.builder(withAPIKey: "YOUR_API_KEY")
.with(transactionFinishBehavior: .manual)
try await Adapty.activate(with: configBuilder.build())
```
When using manual transaction finishing, you need to implement the `onUnfinishedTransaction` delegate method to handle unfinished transactions:
```swift showLineNumbers title="Swift"
extension YourApp: AdaptyDelegate {
func onUnfinishedTransaction(_ transaction: AdaptyUnfinishedTransaction) async {
// Perform your custom validation logic here
// When ready, finish the transaction
await transaction.finish()
}
}
```
To get all current unfinished transactions, use the `getUnfinishedTransactions()` method:
```swift
let unfinishedTransactions = try await Adapty.getUnfinishedTransactions()
```
---
# File: implement-observer-mode
---
---
title: "Implement Observer mode in iOS SDK"
description: "Implement observer mode in Adapty to track user subscription events in iOS SDK."
---
If you already have your own purchase infrastructure and aren't ready to fully switch to Adapty, you can explore [Observer mode](observer-vs-full-mode). In its basic form, Observer Mode offers advanced analytics and seamless integration with attribution and analytics systems.
If this meets your needs, you only need to:
1. Turn it on when configuring the Adapty SDK by setting the `observerMode` parameter to `true`.
2. [Report transactions](report-transactions-observer-mode) from your existing purchase infrastructure to Adapty.
If you also need paywalls and A/B testing, additional setup is required, as described below.
## Observer mode setup
Turn on the Observer mode if you handle purchases and subscription status yourself and use Adapty for sending subscription events and analytics.
:::important
When running in Observer mode, Adapty SDK won't close any transactions, so make sure you're handling it.
:::
```swift showLineNumbers
@main
struct YourApp: App {
init() {
// Configure Adapty SDK
let configurationBuilder = AdaptyConfiguration
.builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") // Get from Adapty dashboard
.with(observerMode: true)
let config = configurationBuilder.build()
// Activate Adapty SDK asynchronously
Task {
do {
try await Adapty.activate(with: configurationBuilder)
} catch {
// Handle error appropriately for your app
print("Adapty activation failed: ", error)
}
}
var body: some Scene {
WindowGroup {
// Your content view
}
}
}
}
```
```swift showLineNumbers
Task {
do {
let configurationBuilder = AdaptyConfiguration
.builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") // Get from Adapty dashboard
.with(observerMode: true)
let config = configurationBuilder.build()
try await Adapty.activate(with: config)
} catch {
// Handle error appropriately for your app
print("Adapty activation failed: ", error)
}
}
```
Parameters:
| Parameter | Description |
| --------------------------- | ------------------------------------------------------------ |
| observerMode | A boolean value that controls [Observer mode](observer-vs-full-mode). The default value is `false`. |
## Using Adapty paywalls in Observer Mode
If you also want to use Adapty's paywalls and A/B testing features, you can — but it requires some extra setup in Observer mode. Here's what you'll need to do in addition to the steps above:
1. Display paywalls as usual for [remote config paywalls](present-remote-config-paywalls). For Paywall Builder paywalls, follow the specific setup guides for [iOS](ios-present-paywall-builder-paywalls-in-observer-mode).
3. [Associate paywalls](report-transactions-observer-mode) with purchase transactions.
---
# File: report-transactions-observer-mode
---
---
title: "Report transactions in Observer Mode in iOS SDK"
description: "Report purchase transactions in Adapty Observer Mode for user insights and revenue tracking in iOS SDK."
---
In Observer mode, the Adapty SDK can't track purchases made through your existing purchase system on its own. You need to report transactions from your app store. It's crucial to set this up **before** releasing your app to avoid errors in analytics.
Use `reportTransaction` to explicitly report each transaction for Adapty to recognize it.
:::warning
**Don't skip transaction reporting!**
If you don't call `reportTransaction`, Adapty won't recognize the transaction, it won't appear in analytics, and it won't be sent to integrations.
:::
If you use Adapty paywalls, include the `variationId` when reporting a transaction. This links the purchase to the paywall that triggered it, ensuring accurate paywall analytics.
```swift showLineNumbers
do {
// every time when calling transasction.finish()
try await Adapty.reportTransaction(transaction, withVariationId: )
} catch {
// handle the error
}
```
Parameters:
| Parameter | Presence | Description |
| --------------- | -------- | ------------------------------------------------------------ |
| **transaction** | required |
For StoreKit 1: SKPaymentTransaction.
For StoreKit 2: Transaction.
|
| **variationId** | optional | The unique ID of the paywall variation. Retrieve it from the `variationId` property of the [AdaptyPaywall](https://swift.adapty.io/documentation/adapty/adaptypaywall) object. |
In Observer mode, the Adapty SDK can't track purchases made through your existing purchase system on its own. You need to report transactions from your app store or restore them. It's crucial to set this up **before** releasing your app to avoid errors in analytics.
Use `reportTransaction` to send the transaction data to Adapty.
:::warning
**Don't skip transaction reporting!**
If you don't call `reportTransaction`, Adapty won't recognize the transaction, it won't appear in analytics, and it won't be sent to integrations.
:::
If you use Adapty paywalls, include the `withVariationId` when reporting a transaction. This links the purchase to the paywall that triggered it, ensuring accurate paywall analytics.
```swift showLineNumbers
do {
// every time when calling transasction.finish()
try await Adapty.reportTransaction(transaction, withVariationId: )
} catch {
// handle the error
}
```
Parameters:
| Parameter | Presence | Description |
| --------------- | -------- | ------------------------------------------------------------ |
| **transaction** | required |
For StoreKit 1: SKPaymentTransaction.
For StoreKit 2: Transaction.
|
| **variationId** | optional | The unique ID of the paywall variation. Retrieve it from the `variationId` property of the [AdaptyPaywall](https://swift.adapty.io/documentation/adapty/adaptypaywall) object. |
**Reporting transactions**
- Versions up to 3.1.x automatically listen for transactions in the App Store, so manual reporting is not required.
- Version 3.2 does not support Observer Mode.
**Associating paywalls to transactions**
Adapty SDK cannot determine the source of purchases, as you are the one processing them. Therefore, if you intend to use paywalls and/or A/B tests in Observer mode, you need to associate the transaction coming from your app store with the corresponding paywall in your mobile app code. This is important to get right before releasing your app, otherwise, it will lead to errors in analytics.
```swift
let variationId = paywall.variationId
// There are two overloads: for StoreKit 1 and StoreKit 2
Adapty.setVariationId(variationId, forPurchasedTransaction: transactionId) { error in
if error == nil {
// successful binding
}
}
```
Request parameters:
| Parameter | Presence | Description |
| ------------- | -------- | ------------------------------------------------------------ |
| variationId | required | The string identifier of the variation. You can get it using `variationId` property of the [AdaptyPaywall](https://swift.adapty.io/documentation/adapty/adaptypaywall) object. |
| transactionId | required |
For StoreKit 1: an [SKPaymentTransaction](https://developer.apple.com/documentation/storekit/skpaymenttransaction) object.
For StoreKit 2: [Transaction](https://developer.apple.com/documentation/storekit/transaction) object.
|
---
# File: ios-present-paywall-builder-paywalls-in-observer-mode
---
---
title: "Present Paywall Builder paywalls in Observer mode in iOS SDK"
description: "Learn how to present PB paywalls in observer mode for better insights."
---
If you've customized a paywall using the Paywall Builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown.
:::warning
This section refers to [Observer mode](observer-vs-full-mode) only. If you do not work in the Observer mode, refer to the [iOS - Present Paywall Builder paywalls](ios-present-paywalls).
:::
Before you start presenting paywalls (Click to Expand)
1. Set up initial integration of Adapty [with the Google Play](initial-android) and [with the App Store](initial_ios).
2. Install and configure Adapty SDK. Make sure to set the `observerMode` parameter to `true`. Refer to our framework-specific instructions for [iOS](sdk-installation-ios#configure-adapty-sdk).
3. [Create products](create-product) in the Adapty Dashboard.
4. [Configure paywalls, assign products to them](create-paywall), and customize them using Paywall Builder in the Adapty Dashboard.
5. [Create placements and assign your paywalls to them](create-placement) in the Adapty Dashboard.
6. [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls) in your mobile app code.
1. Implement the `AdaptyObserverModeResolver` object:
```swift showLineNumbers title="Swift"
func observerMode(didInitiatePurchase product: AdaptyPaywallProduct,
onStartPurchase: @escaping () -> Void,
onFinishPurchase: @escaping () -> Void) {
// use the product object to handle the purchase
// use the onStartPurchase and onFinishPurchase callbacks to notify AdaptyUI about the process of the purchase
}
func observerModeDidInitiateRestorePurchases(onStartRestore: @escaping () -> Void,
onFinishRestore: @escaping () -> Void) {
// use the onStartRestore and onFinishRestore callbacks to notify AdaptyUI about the process of the restore
}
```
The `observerMode(didInitiatePurchase:onStartPurchase:onFinishPurchase:)` event will inform you that the user has initiated a purchase. You can trigger your custom purchase flow in response to this callback.
The `observerModeDidInitiateRestorePurchases(onStartRestore:onFinishRestore:)` event will inform you that the user has initiated a restore. You can trigger your custom restore flow in response to this callback.
Also, remember to invoke the following callbacks to notify AdaptyUI about the process of the purchase or restore. This is necessary for proper paywall behavior, such as showing the loader, among other things:
| Callback | Description |
| :----------------- | :------------------------------------------------------------------------------- |
| onStartPurchase() | The callback should be invoked to notify AdaptyUI that the purchase is started. |
| onFinishPurchase() | The callback should be invoked to notify AdaptyUI that the purchase is finished. |
| onStartRestore() | The callback should be invoked to notify AdaptyUI that the restore is started. |
| onFinishRestore() | The callback should be invoked to notify AdaptyUI that the restore is finished. |
2. Create a paywall configuration object:
```swift showLineNumbers title="Swift"
do {
let paywallConfiguration = try AdaptyUI.getPaywallConfiguration(
forPaywall: ,
observerModeResolver:
)
} catch {
// handle the error
}
```
Request parameters:
| Parameter | Presence | Description |
| :----------------------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Paywall** | required | An `AdaptyPaywall` object to obtain a controller for the desired paywall. |
| **ObserverModeResolver** | required | The `AdaptyObserverModeResolver` object you've implemented in the previous step |
3. Initialize the visual paywall you want to display by using the `.paywallController(for:products:viewConfiguration:delegate:)` method:
```swift showLineNumbers title="Swift"
import Adapty
import AdaptyUI
let visualPaywall = AdaptyUI.paywallController(
with: ,
delegate:
)
```
Request parameters:
| Parameter | Presence | Description |
| :----------------------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Paywall Configuration** | required | An `AdaptyUI.PaywallConfiguration` object containing visual details of the paywall. Use the `AdaptyUI.getPaywallConfiguration(forPaywall:locale:)` method. Refer to [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls) topic for more details. |
| **Delegate** | required | An `AdaptyPaywallControllerDelegate` to listen to paywall events. Refer to [Handling paywall events](ios-handling-events) topic for more details. |
Returns:
| Object | Description |
| :---------------------- | :--------------------------------------------------- |
| AdaptyPaywallController | An object, representing the requested paywall screen |
After the object has been successfully created, you can display it like so:
```swift showLineNumbers title="Swift"
present(visualPaywall, animated: true)
```
:::warning
Don't forget to [Associate paywalls to purchase transactions](report-transactions-observer-mode). Otherwise, Adapty will not determine the source paywall of the purchase.
:::
In order to display the visual paywall on the device screen, use the `.paywall` modifier in SwiftUI:
```swift showLineNumbers title="SwiftUI"
@State var paywallPresented = false
var body: some View {
Text("Hello, AdaptyUI!")
.paywall(
isPresented: $paywallPresented,
paywallConfiguration: ,
didPerformAction: { action in
switch action {
case .close:
paywallPresented = false
default:
// Handle other actions
break
}
},
didFinishRestore: { profile in /* check access level and dismiss */ },
didFailRestore: { error in /* handle the error */ },
didFailRendering: { error in paywallPresented = false }
)
}
```
Request parameters:
| Parameter | Presence | Description |
| :----------------------- | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Paywall Configuration** | required | An `AdaptyUI.PaywallConfiguration` object containing visual details of the paywall. Use the `AdaptyUI.getPaywallConfiguration(forPaywall:locale:)` method. Refer to [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls) topic for more details. |
| **Products** | optional | Provide an array of `AdaptyPaywallProducts` to optimize the display timing of products on the screen. If `nil` is passed, AdaptyUI will automatically fetch the necessary products. |
| **TagResolver** | optional | Define a dictionary of custom tags and their resolved values. Custom tags serve as placeholders in the paywall content, dynamically replaced with specific strings for personalized content within the paywall. Refer to [Custom tags in Paywall Builder](custom-tags-in-paywall-builder) topic for more details. |
| **ObserverModeResolver** | optional | The `AdaptyObserverModeResolver` object you've implemented in the previous step |
Closure parameters:
| Closure parameter | Description |
| :------------------- | :-------------------------------------------------------------------------------- |
| **didFinishRestore** | If Adapty.restorePurchases() succeeds, this callback will be invoked. |
| **didFailRestore** | If Adapty.restorePurchases() fails, this callback will be invoked. |
| **didFailRendering** | If an error occurs during the interface rendering, this callback will be invoked. |
Refer to the [iOS - Handling events](ios-handling-events) topic for other closure parameters.
:::warning
Don't forget to [Associate paywalls to purchase transactions](report-transactions-observer-mode). Otherwise, Adapty will not determine the source paywall of the purchase.
:::
Before you start presenting paywalls (Click to Expand)
1. Set up initial integration of Adapty [with the Google Play](initial-android) and [with the App Store](initial_ios).
1. Install and configure Adapty SDK. Make sure to set the `observerMode` parameter to `true`. Refer to our framework-specific instructions for [iOS](sdk-installation-ios#configure-adapty-sdk), [React Native](sdk-installation-reactnative#configure-adapty-sdks), [Flutter](sdk-installation-flutter#configure-adapty-sdk), and [Unity](sdk-installation-unity#configure-adapty-sdk).
2. [Create products](create-product) in the Adapty Dashboard.
3. [Configure paywalls, assign products to them](create-paywall), and customize them using Paywall Builder in the Adapty Dashboard.
4. [Create placements and assign your paywalls to them](create-placement) in the Adapty Dashboard.
5. [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls) in your mobile app code.
1. Implement the `AdaptyObserverModeDelegate` object:
```swift showLineNumbers title="Swift"
func paywallController(_ controller: AdaptyPaywallController,
didInitiatePurchase product: AdaptyPaywallProduct,
onStartPurchase: @escaping () -> Void,
onFinishPurchase: @escaping () -> Void) {
// use the product object to handle the purchase
// use the onStartPurchase and onFinishPurchase callbacks to notify AdaptyUI about the process of the purchase
}
```
The `paywallController(_:didInitiatePurchase:onStartPurchase:onFinishPurchase:)` event will inform you that the user has initiated a purchase. You can trigger your custom purchase flow in response to this event.
Also, remember to invoke the following callbacks to notify AdaptyUI about the process of the purchase. This is necessary for proper paywall behavior, such as showing the loader, among other things:
| Callback | Description |
| :--------------- | :------------------------------------------------------------------------------- |
| onStartPurchase | The callback should be invoked to notify AdaptyUI that the purchase is started. |
| onFinishPurchase | The callback should be invoked to notify AdaptyUI that the purchase is finished. |
2. Initialize the visual paywall you want to display by using the `.paywallController(for:products:viewConfiguration:delegate:observerModeDelegate:)` method:
```swift showLineNumbers title="Swift"
import Adapty
import AdaptyUI
let visualPaywall = AdaptyUI.paywallController(
for: ,
products: ,
viewConfiguration: ,
delegate:
observerModeDelegate:
)
```
Request parameters:
| Parameter | Presence | Description |
| :----------------------- | :------- | :----------------------------------------------------------- |
| **Paywall** | required | An `AdaptyPaywall` object to obtain a controller for the desired paywall. |
| **Products** | optional | Provide an array of `AdaptyPaywallProducts` to optimize the display timing of products on the screen. If `nil` is passed, AdaptyUI will automatically fetch the necessary products. |
| **ViewConfiguration** | required | An `AdaptyUI.LocalizedViewConfiguration` object containing visual details of the paywall. Use the `AdaptyUI.getViewConfiguration(paywall:locale:)` method. Refer to [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls) topic for more details. |
| **Delegate** | required | An `AdaptyPaywallControllerDelegate` to listen to paywall events. Refer to [Handling paywall events](ios-handling-events) topic for more details. |
| **ObserverModeDelegate** | required | The `AdaptyObserverModeDelegate` object you've implemented in the previous step |
| **TagResolver** | optional | Define a dictionary of custom tags and their resolved values. Custom tags serve as placeholders in the paywall content, dynamically replaced with specific strings for personalized content within the paywall. Refer to [Custom tags in Paywall Builder](custom-tags-in-paywall-builder) topic for more details. |
Returns:
| Object | Description |
| :---------------------- | :--------------------------------------------------- |
| AdaptyPaywallController | An object, representing the requested paywall screen |
After the object has been successfully created, you can display it like so:
```swift showLineNumbers title="Swift"
present(visualPaywall, animated: true)
```
:::warning
Don't forget to [Associate paywalls to purchase transactions](report-transactions-observer-mode). Otherwise, Adapty will not determine the source paywall of the purchase.
:::
In order to display the visual paywall on the device screen, use the `.paywall` modifier in SwiftUI:
```swift showLineNumbers title="SwiftUI"
@State var paywallPresented = false
var body: some View {
Text("Hello, AdaptyUI!")
.paywall(
isPresented: $paywallPresented,
paywall: ,
configuration: ,
didPerformAction: { action in
switch action {
case .close:
paywallPresented = false
default:
// Handle other actions
break
}
},
didFinishRestore: { profile in /* check access level and dismiss */ },
didFailRestore: { error in /* handle the error */ },
didFailRendering: { error in paywallPresented = false },
observerModeDidInitiatePurchase: { product, onStartPurchase, onFinishPurchase in
// use the product object to handle the purchase
// use the onStartPurchase and onFinishPurchase callbacks to notify AdaptyUI about the process of the purchase
},
)
}
```
Request parameters:
| Parameter | Presence | Description |
| :---------------- | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Paywall** | required | An `AdaptyPaywall` object to obtain a controller for the desired paywall. |
| **Product** | optional | Provide an array of `AdaptyPaywallProducts` to optimize the display timing of products on the screen. If `nil` is passed, AdaptyUI will automatically fetch the necessary products. |
| **Configuration** | required | An `AdaptyUI.LocalizedViewConfiguration` object containing visual details of the paywall. Use the `AdaptyUI.getViewConfiguration(paywall:locale:)` method. Refer to [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls) topic for more details. |
| **TagResolver** | optional | Define a dictionary of custom tags and their resolved values. Custom tags serve as placeholders in the paywall content, dynamically replaced with specific strings for personalized content within the paywall. Refer to [Custom tags in paywall builder](custom-tags-in-paywall-builder) topic for more details. |
Closure parameters:
| Closure parameter | Description |
| :---------------------------------- | :-------------------------------------------------------------------------------- |
| **didFinishRestore** | If Adapty.restorePurchases() succeeds, this callback will be invoked. |
| **didFailRestore** | If Adapty.restorePurchases() fails, this callback will be invoked. |
| **didFailRendering** | If an error occurs during the interface rendering, this callback will be invoked. |
| **observerModeDidInitiatePurchase** | This callback is invoked when a user initiates a purchase. |
Refer to the [iOS - Handling events](ios-handling-events) topic for other closure parameters.
:::warning
Don't forget to [Associate paywalls to purchase transactions](report-transactions-observer-mode). Otherwise, Adapty will not determine the source paywall of the purchase.
:::
---
# File: ios-troubleshoot-purchases
---
---
title: "Troubleshoot purchases in iOS SDK"
description: "Troubleshoot purchases in iOS SDK"
---
This guide helps you resolve common issues when implementing purchases manually in the iOS SDK.
## AdaptyError.cantMakePayments in observer mode
**Issue**: You're getting `AdaptyError.cantMakePayments` when using `makePurchase` in observer mode.
**Reason**: In observer mode, you should handle purchases on your side, not use Adapty's `makePurchase` method.
**Solution**: If you use `makePurchase` for purchases, turn off the observer mode. You need either to use `makePurchase` or handle purchases on your side in the observer mode. See [Implement Observer mode](implement-observer-mode) for more details.
## Not found makePurchasesCompletionHandlers
**Issue**: You're encountering issues with `makePurchasesCompletionHandlers` not being found.
**Reason**: This is typically related to sandbox testing issues.
**Solution**: Create a new sandbox user and try again. This often resolves sandbox-related purchase completion handler issues.
## Other issues
**Issue**: You're experiencing other purchase-related problems not covered above.
**Solution**: Migrate the SDK to the latest version using the [migration guides](ios-sdk-migration-guides) if needed. Many issues are resolved in newer SDK versions.
---
# File: identifying-users
---
---
title: "Identify users in iOS SDK"
description: "Identify users in Adapty to improve personalized subscription experiences."
---
Adapty creates an internal profile ID for every user. However, if you have your own authentication system, you should set your own Customer User ID. You can find users by their Customer User ID in the [Profiles](profiles-crm) section and use it in the [server-side API](getting-started-with-server-side-api), which will be sent to all integrations.
## Set customer user ID on configuration
If you have a user ID during configuration, just pass it as `customerUserId` parameter to `.activate()` method:
```swift showLineNumbers
// In your AppDelegate class:
let configurationBuilder =
AdaptyConfiguration
.builder(withAPIKey: "PUBLIC_SDK_KEY")
.with(customerUserId: "YOUR_USER_ID")
do {
try await Adapty.activate(with: configurationBuilder.build())
} catch {
// handle the error
}
```
```swift showLineNumbers
// In your AppDelegate class:
let configurationBuilder =
AdaptyConfiguration
.builder(withAPIKey: "PUBLIC_SDK_KEY")
.with(customerUserId: "YOUR_USER_ID")
Adapty.activate(with: configurationBuilder.build()) { error in
// handle the error
}
```
:::tip
Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality.
:::
## Set customer user ID after configuration
If you don't have a user ID in the SDK configuration, you can set it later at any time with the `.identify()` method. The most common cases for using this method are after registration or authorization, when the user switches from being an anonymous user to an authenticated user.
```swift showLineNumbers
do {
try await Adapty.identify("YOUR_USER_ID")
} catch {
// handle the error
}
```
```swift showLineNumbers
Adapty.identify("YOUR_USER_ID") { error in
if let error {
// handle the error
}
}
```
Request parameters:
- **Customer User ID** (required): a string user identifier.
:::warning
Resubmitting of significant user data
In some cases, such as when a user logs into their account again, Adapty's servers already have information about that user. In these scenarios, the Adapty SDK will automatically switch to work with the new user. If you passed any data to the anonymous user, such as custom attributes or attributions from third-party networks, you should resubmit that data for the identified user.
It's also important to note that you should re-request all paywalls and products after identifying the user, as the new user's data may be different.
:::
## Logging out and logging in
You can logout the user anytime by calling `.logout()` method:
```swift showLineNumbers
do {
try await Adapty.logout()
} catch {
// handle the error
}
```
```swift showLineNumbers
Adapty.logout { error in
if error == nil {
// successful logout
}
}
```
You can then login the user using `.identify()` method.
## Set appAccountToken
The [`appAccountToken`](https://developer.apple.com/documentation/storekit/product/purchaseoption/appaccounttoken(_:)) is a UUID that helps Apple's StoreKit 2 identify users across app installations and devices.
Starting from the Adapty iOS SDK 3.10.2, you can pass the `appAccountToken` when configuring the SDK or when identifying a user:
```swift showLineNumbers
// During configuration:
let configurationBuilder =
AdaptyConfiguration
.builder(withAPIKey: "PUBLIC_SDK_KEY")
.with(customerUserId: "YOUR_USER_ID", withAppAccountToken: UUID())
do {
try await Adapty.activate(with: configurationBuilder.build())
} catch {
// handle the error
}
// Or when identifying a user:
do {
try await Adapty.identify("YOUR_USER_ID", withAppAccountToken: UUID())
} catch {
// handle the error
}
```
```swift showLineNumbers
// During configuration:
let configurationBuilder =
AdaptyConfiguration
.builder(withAPIKey: "PUBLIC_SDK_KEY")
.with(customerUserId: "YOUR_USER_ID", withAppAccountToken: UUID())
Adapty.activate(with: configurationBuilder.build()) { error in
// handle the error
}
// Or when identifying a user:
Adapty.identify("YOUR_USER_ID", withAppAccountToken: UUID()) { error in
if let error {
// handle the error
}
}
```
You can then login the user using `.identify()` method.
---
# File: setting-user-attributes
---
---
title: "Set user attributes in iOS SDK"
description: "Learn how to set user attributes in Adapty to enable better audience segmentation."
---
You can set optional attributes such as email, phone number, etc, to the user of your app. You can then use attributes to create user [segments](segments) or just view them in CRM.
### Setting user attributes
To set user attributes, call `.updateProfile()` method:
```swift showLineNumbers
let builder = AdaptyProfileParameters.Builder()
.with(email: "email@email.com")
.with(phoneNumber: "+18888888888")
.with(firstName: "John")
.with(lastName: "Appleseed")
.with(gender: .other)
.with(birthday: Date())
do {
try await Adapty.updateProfile(params: builder.build())
} catch {
// handle the error
}
```
```swift showLineNumbers
let builder = AdaptyProfileParameters.Builder()
.with(email: "email@email.com")
.with(phoneNumber: "+18888888888")
.with(firstName: "John")
.with(lastName: "Appleseed")
.with(gender: .other)
.with(birthday: Date())
Adapty.updateProfile(params: builder.build()) { error in
if error != nil {
// handle the error
}
}
```
Please note that the attributes that you've previously set with the `updateProfile` method won't be reset.
:::tip
Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality.
:::
### The allowed keys list
The allowed keys `` of `AdaptyProfileParameters.Builder` and the values `` are listed below:
| Key | Value |
|---|-----|
|
email
phoneNumber
firstName
lastName
| String |
| gender | Enum, allowed values are: `female`, `male`, `other` |
| birthday | Date |
### Custom user attributes
You can set your own custom attributes. These are usually related to your app usage. For example, for fitness applications, they might be the number of exercises per week, for language learning app user's knowledge level, and so on. You can use them in segments to create targeted paywalls and offers, and you can also use them in analytics to figure out which product metrics affect the revenue most.
```swift showLineNumbers
do {
builder = try builder.with(customAttribute: "value1", forKey: "key1")
} catch {
// handle key/value validation error
}
```
To remove existing key, use `.withRemoved(customAttributeForKey:)` method:
```swift showLineNumbers
do {
builder = try builder.withRemoved(customAttributeForKey: "key2")
} catch {
// handle error
}
```
Sometimes you need to figure out what custom attributes have already been installed before. To do this, use the `customAttributes` field of the `AdaptyProfile` object.
:::warning
Keep in mind that the value of `customAttributes` may be out of date since the user attributes can be sent from different devices at any time so the attributes on the server might have been changed after the last sync.
:::
### Limits
- Up to 30 custom attributes per user
- Key names are up to 30 characters long. The key name can include alphanumeric characters and any of the following: `_` `-` `.`
- Value can be a string or float with no more than 50 characters.
---
# File: subscription-status
---
---
title: "Check subscription status in iOS SDK"
description: "Track and manage user subscription status in Adapty for improved customer retention."
---
With Adapty, keeping track of subscription status is made easy. You don't have to manually insert product IDs into your code. Instead, you can effortlessly confirm a user's subscription status by checking for an active [access level](access-level).
Before you start checking subscription status, set up [App Store Server Notifications](enable-app-store-server-notifications).
## Access level and the AdaptyProfile object
Access levels are properties of the [AdaptyProfile](https://swift.adapty.io/documentation/adapty/adaptyprofile) object. We recommend retrieving the profile when your app starts, such as when you [identify a user](identifying-users#setting-customer-user-id-on-configuration) , and then updating it whenever changes occur. This way, you can use the profile object without repeatedly requesting it.
To be notified of profile updates, listen for profile changes as described in the [Listening for profile updates, including access levels](subscription-status#listening-for-subscription-status-updates) section below.
:::tip
Want to see a real-world example of how Adapty SDK is integrated into a mobile app? Check out our [sample apps](sample-apps), which demonstrate the full setup, including displaying paywalls, making purchases, and other basic functionality.
:::
## Retrieving the access level from the server
To get the access level from the server, use the `.getProfile()` method:
```swift showLineNumbers
do {
let profile = try await Adapty.getProfile()
if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false {
// grant access to premium features
}
} catch {
// handle the error
}
```
```swift showLineNumbers
Adapty.getProfile { result in
if let profile = try? result.get() {
// check the access
profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false {
// grant access to premium features
}
}
}
```
Response parameters:
| Parameter | Description |
| --------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Profile |
An [AdaptyProfile](https://swift.adapty.io/documentation/adapty/adaptyprofile) object. Generally, you have to check only the access level status of the profile to determine whether the user has premium access to the app.
The `.getProfile` method provides the most up-to-date result as it always tries to query the API. If for some reason (e.g. no internet connection), the Adapty SDK fails to retrieve information from the server, the data from the cache will be returned. It is also important to note that the Adapty SDK updates `AdaptyProfile` cache regularly, to keep this information as up-to-date as possible.
|
The `.getProfile()` method provides you with the user profile from which you can get the access level status. You can have multiple access levels per app. For example, if you have a newspaper app and sell subscriptions to different topics independently, you can create access levels "sports" and "science". But most of the time, you will only need one access level, in that case, you can just use the default "premium" access level.
Here is an example for checking for the default "premium" access level:
```swift showLineNumbers
do {
let profile = try await Adapty.getProfile()
let isPremium = profile.accessLevels["premium"]?.isActive ?? false
// grant access to premium features
} catch {
// handle the error
}
```
```swift showLineNumbers
Adapty.getProfile { result in
if let profile = try? result.get(),
profile.accessLevels["premium"]?.isActive ?? false {
// grant access to premium features
}
}
```
### Listening for subscription status updates
Whenever the user's subscription changes, Adapty fires an event.
To receive messages from Adapty, you need to make some additional configuration:
```swift showLineNumbers
Adapty.delegate = self
// To receive subscription updates, extend `AdaptyDelegate` with this method:
nonisolated func didLoadLatestProfile(_ profile: AdaptyProfile) {
// handle any changes to subscription state
}
```
Adapty also fires an event at the start of the application. In this case, the cached subscription status will be passed.
### Subscription status cache
The cache implemented in the Adapty SDK stores the subscription status of the profile. This means that even if the server is unavailable, the cached data can be accessed to provide information about the profile's subscription status.
However, it's important to note that direct data requests from the cache are not possible. The SDK periodically queries the server every minute to check for any updates or changes related to the profile. If there are any modifications, such as new transactions or other updates, they will be sent to the cached data in order to keep it synchronized with the server.
---
# File: ios-deal-with-att
---
---
title: "Deal with ATT in iOS SDK"
description: "Get started with Adapty on iOS to streamline subscription setup and management."
---
If your application uses AppTrackingTransparency framework and presents an app-tracking authorization request to the user, then you should send the [authorization status](https://developer.apple.com/documentation/apptrackingtransparency/attrackingmanager/authorizationstatus/) to Adapty.
```swift showLineNumbers
let builder = AdaptyProfileParameters.Builder()
.with(appTrackingTransparencyStatus: .authorized)
do {
try await Adapty.updateProfile(params: builder.build())
} catch {
// handle the error
}
```
```swift showLineNumbers
if #available(iOS 14, macOS 11.0, *) {
let builder = AdaptyProfileParameters.Builder()
.with(appTrackingTransparencyStatus: .authorized)
Adapty.updateProfile(params: builder.build()) { [weak self] error in
if error != nil {
// handle the error
}
}
}
```
:::warning
We strongly recommend that you send this value as early as possible when it changes, only in that case the data will be sent in a timely manner to the integrations you have configured.
:::
---
# File: kids-mode
---
---
title: "Kids Mode in iOS SDK"
description: "Easily enable Kids Mode to comply with Apple policies. No IDFA or ad data collected in iOS SDK."
---
If your iOS application is intended for kids, you must follow the policies of [Apple](https://developer.apple.com/kids/). If you're using the Adapty SDK, a few simple steps will help you configure it to meet these policies and pass app store reviews.
## What's required?
You need to configure the Adapty SDK to disable the collection of:
- [IDFA (Identifier for Advertisers)](https://en.wikipedia.org/wiki/Identifier_for_Advertisers)
- [IP address](https://www.ftc.gov/system/files/ftc_gov/pdf/p235402_coppa_application.pdf)
In addition, we recommend using customer user ID carefully. User ID in format `` will be definitely treated as gathering personal data as well as using email. For Kids Mode, a best practice is to use randomized or anonymized identifiers (e.g., hashed IDs or device-generated UUIDs) to ensure compliance.
## Enabling Kids Mode
### Updates in the Adapty Dashboard
In the Adapty Dashboard, you need to disable the IP address collection. To do this, go to [App settings](https://app.adapty.io/settings/general) and click **Disable IP address collection** under **Collect users' IP address**.
### Updates in your mobile app code
In order to comply with policies, disable the collection of the user's IDFA and IP address.
If you use Swift Package Manager, you can enable Kids Mode by selecting the **Adapty_KidsMode** module in Xcode when installing the SDK.
In Xcode, go to **File** -> **Add Package Dependency...**. Note that the steps to add package dependencies may vary between Xcode versions, so refer to Xcode documentation if needed.
1. Enter the repository URL:
```
https://github.com/adaptyteam/AdaptySDK-iOS.git
```
2. Select the version (latest stable version is recommended) and click **Add Package**.
3. In the **Choose Package Products** window, select the modules you need:
- **Adapty_KidsMode** (core module)
- **AdaptyUI** (optional - only if you plan to use Paywall Builder)
You won't need any other packages.
4. Click **Add Package** to complete the installation.
1. Update your Podfile:
- If you **don't** have a `post_install` section, add the entire code block below.
- If you **do** have a `post_install` section, merge the highlighted lines into it.
```ruby showLineNumbers title="Podfile"
post_install do |installer|
installer.pods_project.targets.each do |target|
// highlight-start
if target.name == 'Adapty'
target.build_configurations.each do |config|
config.build_settings['OTHER_SWIFT_FLAGS'] ||= ['$(inherited)']
config.build_settings['OTHER_SWIFT_FLAGS'] << '-DADAPTY_KIDS_MODE'
end
end
// highlight-end
end
end
```
2. Run the following command to apply the changes:
```sh showLineNumbers title="Shell"
pod install
```
---
# File: get-onboardings
---
---
title: "Fetch onboardings and their configuration"
description: "Learn how to retrieve onboardings in Adapty for."
---
After [you designed the visual part for your onboarding](design-onboarding.md) with the builder in the Adapty Dashboard, you can display it in your mobile app. The first step in this process is to get the onboarding associated with the placement and its view configuration as described below.
Before you start, ensure that:
1. You have installed [Adapty iOS, Android, React Native, or Flutter SDK](installation-of-adapty-sdks.md) version 3.8.0 or higher.
2. You have [created an onboarding](create-onboarding.md).
3. You have added the onboarding to a [placement](placements.md).
## Fetch onboarding
When you create an [onboarding](onboardings.md) with our no-code builder, it's stored as a container with configuration that your app needs to fetch and display. This container manages the entire experience - what content appears, how it's presented, and how user interactions (like quiz answers or form inputs) are processed. The container also automatically tracks analytics events, so you don't need to implement separate view tracking.
For best performance, fetch the onboarding configuration early to give images enough time to download before showing to users.
To get an onboarding, use the `getOnboarding` method:
```swift showLineNumbers
do {
let onboarding = try await Adapty.getOnboarding(placementId: "YOUR_PLACEMENT_ID")
// the requested onboarding
} catch {
// handle the error
}
```
Parameters:
| Parameter | Presence | Description |
|---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. |
| **locale** |
optional
default: `en`
|
The identifier of the onboarding localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores onboardings locally in two layers: regularly updated cache described above and fallback onboardings. We also use CDN to fetch onboardings faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your onboardings while ensuring reliability even in cases where internet connection is scarce.
|
| **loadTimeout** | default: 5 sec |
This value limits the timeout for this method. If the timeout is reached, cached data or local fallback will be returned.
Note that in rare cases this method can timeout slightly later than specified in `loadTimeout`, since the operation may consist of different requests under the hood.
|
Response parameters:
| Parameter | Description |
|:----------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------|
| Onboarding | An [`AdaptyOnboarding`](https://swift.adapty.io/documentation/adapty/adaptyonboarding) object with: the onboarding identifier and configuration, remote config, and several other properties. |
## Speed up onboarding fetching with default audience onboarding
Typically, onboardings are fetched almost instantly, so you don't need to worry about speeding up this process. However, in cases where you have numerous audiences and onboardings, and your users have a weak internet connection, fetching a onboarding may take longer than you'd like. In such situations, you might want to display a default onboarding to ensure a smooth user experience rather than showing no onboarding at all.
To address this, you can use the `getOnboardingForDefaultAudience` method, which fetches the onboarding of the specified placement for the **All Users** audience. However, it's crucial to understand that the recommended approach is to fetch the onboarding by the `getOnboarding` method, as detailed in the [Fetch Onboarding](#fetch-onboarding) section above.
:::warning
Consider using `getOnboarding` instead of `getOnboardingForDefaultAudience`, as the latter has important limitations:
- **Compatibility issues**: May create problems when supporting multiple app versions, requiring either backward-compatible designs or accepting that older versions might display incorrectly.
- **No personalization**: Only shows content for the "All Users" audience, removing targeting based on country, attribution, or custom attributes.
If faster fetching outweighs these drawbacks for your use case, use `getOnboardingForDefaultAudience` as shown below. Otherwise, use `getOnboarding` as described [above](#fetch-onboarding).
:::
```swift showLineNumbers
Adapty.getOnboardingForDefaultAudience(placementId: "YOUR_PLACEMENT_ID") { result in
switch result {
case let .success(onboarding):
// the requested onboarding
case let .failure(error):
// handle the error
}
}
```
Parameters:
| Parameter | Presence | Description |
|---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. |
| **locale** |
optional
default: `en`
|
The identifier of the onboarding localization. This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this option because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores onboardings locally in two layers: regularly updated cache described above and fallback onboardings. We also use CDN to fetch onboardings faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your onboardings while ensuring reliability even in cases where internet connection is scarce.
|
---
# File: ios-present-onboardings
---
---
title: "Present onboardings in iOS SDK"
description: "Discover how to present onboardings on iOS to boost conversions and revenue."
---
If you've customized an onboarding using the builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such an onboarding contains both what should be shown within the onboarding and how it should be shown.
Before you start, ensure that:
1. You have installed [Adapty iOS SDK](sdk-installation-ios.md) 3.8.0 or later.
2. You have [created an onboarding](create-onboarding.md).
3. You have added the onboarding to a [placement](placements.md).
## Present onboardings in Swift
In order to display the visual onboarding on the device screen, do the following:
1. Get the onboarding view configuration using the `.getOnboardingConfiguration` method.
2. Initialize the visual onboarding you want to display by using the `.onboardingController` method:
Request parameters:
| Parameter | Presence | Description |
|:-----------------------------|:---------|:------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **onboarding configuration** | required | An `AdaptyUI.OnboardingConfiguration` object containing all the onboarding properties. Use the `AdaptyUI.getOnboardingConfiguration` method to obtain it. |
| **delegate** | required | An `AdaptyOnboardingControllerDelegate` to listen to onboarding events. |
Returns:
| Object | Description |
|:-------------------------------|:--------------------------------------------------------|
| **AdaptyOnboardingController** | An object, representing the requested onboarding screen |
3. After the object has been successfully created, you can display it on the screen of the device:
```swift showLineNumbers title="Swift"
import Adapty
import AdaptyUI
// 0. Get an onboarding if you haven't done it yet
let onboarding = try await Adapty.getOnboarding(placementId: "YOUR_PLACEMENT_ID")
// 1. Obtain the onboarding view configuration:
let configuration = try AdaptyUI.getOnboardingConfiguration(forOnboarding: onboarding)
// 2. Create Onboarding View Controller
let onboardingController = try AdaptyUI.onboardingController(
with: configuration,
delegate:
)
// 3. Present it to the user
present(onboardingController, animated: true)
```
## Present onboardings in SwiftUI
To display the visual onboarding on the device screen in SwiftUI:
```swift showLineNumbers title="SwiftUI"
// 1. Obtain the onboarding view configuration:
let configuration = try AdaptyUI.getOnboardingConfiguration(forOnboarding: onboarding)
// 2. Display the Onboarding View within your view hierarchy
AdaptyOnboardingView(
configuration: configuration,
placeholder: { Text("Your Placeholder View") },
onCloseAction: { action in
// hide the onboarding view
},
onError: { error in
// handle the error
}
)
```
## Add smooth transitions between the splash screen and onboarding
By default, between the splash screen and onboarding, you will see the loading screen until the onboarding is fully loaded. However, if you want to make the transition smoother, you can customize it and either extend the splash screen or display something else.
To do this, define a placeholder (what exactly will be shown while the onboarding is being loaded). If you define a placeholder, the onboarding will be loaded in the background and automatically displayed once ready.
```swift showLineNumbers
extension YourOnboardingManagerClass: AdaptyOnboardingControllerDelegate {
func onboardingsControllerLoadingPlaceholder(
_ controller: AdaptyOnboardingController
) -> UIView? {
// instantiate and return the UIView which will be presented while onboarding is being loaded
}
}
```
```swift showLineNumbers
AdaptyOnboardingView(
configuration: configuration,
placeholder: {
// define your placeholder view, which will be presented while onboarding is being loaded
},
// the rest of the implementation
)
```
## Customize how links open in onboardings
:::important
Customizing how links open in onboardings is supported starting from Adapty SDK v.3.15.1.
:::
By default, links in onboardings open in an in-app browser. This provides a seamless user experience by displaying web pages within your application, allowing users to view them without switching apps.
If you prefer to open links in an external browser instead, you can customize this behavior by setting the `externalUrlsPresentation` parameter to `.externalBrowser`:
```swift showLineNumbers
let configuration = try AdaptyUI.getOnboardingConfiguration(
forOnboarding: onboarding,
externalUrlsPresentation: .externalBrowser // default – .inAppBrowser
)
```
---
# File: ios-handling-onboarding-events
---
---
title: "Handle onboarding events in iOS SDK"
description: "Handle onboarding-related events in iOS using Adapty."
---
Before you start, ensure that:
1. You have installed [Adapty iOS SDK](sdk-installation-ios.md) 3.8.0 or later.
2. You have [created an onboarding](create-onboarding.md).
3. You have added the onboarding to a [placement](placements.md).
Onboardings configured with the builder generate events your app can respond to. Learn how to respond to these events below.
To control or monitor processes occurring on the onboarding screen within your mobile app, implement the `AdaptyOnboardingControllerDelegate` methods.
## Custom actions
In the builder, you can add a **custom** action to a button and assign it an ID.
Then, you can use this ID in your code and handle it as a custom action. For example, if a user taps a custom button, like **Login** or **Allow notifications**, the delegate method `onboardingController` will be triggered with the `.custom(id:)` case and the `actionId` parameter is the **Action ID** from the builder. You can create your own IDs, like "allowNotifications".
```swift showLineNumbers
func onboardingController(_ controller: AdaptyOnboardingController, onCustomAction action: AdaptyOnboardingsCustomAction) {
if action.actionId == "allowNotifications" {
// Request notification permissions
}
}
func onboardingController(_ controller: AdaptyOnboardingController, didFailWithError error: AdaptyUIError) {
// Handle errors
}
```
Event example (Click to expand)
```json
{
"actionId": "allowNotifications",
"meta": {
"onboardingId": "onboarding_123",
"screenClientId": "profile_screen",
"screenIndex": 0,
"screensTotal": 3
}
}
```
## Closing onboarding
Onboarding is considered closed when a user taps a button with the **Close** action assigned.
:::important
Note that you need to manage what happens when a user closes the onboarding. For instance, you need to stop displaying the onboarding itself.
:::
For example:
```swift showLineNumbers
func onboardingController(_ controller: AdaptyOnboardingController, onCloseAction action: AdaptyOnboardingsCloseAction) {
controller.dismiss(animated: true)
}
```
Event example (Click to expand)
```json
{
"action_id": "close_button",
"meta": {
"onboarding_id": "onboarding_123",
"screen_cid": "final_screen",
"screen_index": 3,
"total_screens": 4
}
}
```
## Opening a paywall
:::tip
Handle this event to open a paywall if you want to open it inside the onboarding. If you want to open a paywall after it is closed, there is a more straightforward way to do it – handle [`AdaptyOnboardingsCloseAction`](#closing-onboarding) and open a paywall without relying on the event data.
:::
If a user clicks a button that opens a paywall, you will get a button action ID that you [set up manually](get-paid-in-onboardings.md). The most seamless way to work with paywalls in onboardings is to make the action ID equal to a paywall placement ID. This way, after the `AdaptyOnboardingsOpenPaywallAction`, you can use the placement ID to get and open the paywall right away.
Note that only one view (paywall or onboarding) can be displayed on screen at a time. If you present a paywall on top of an onboarding, you cannot programmatically control the onboarding in the background. Attempting to dismiss the onboarding will close the paywall instead, leaving the onboarding visible. To avoid this, always dismiss the onboarding view before presenting the paywall.
```swift showLineNumbers
func onboardingController(_ controller: AdaptyOnboardingController, onPaywallAction action: AdaptyOnboardingsOpenPaywallAction) {
// Dismiss onboarding before presenting paywall
controller.dismiss(animated: true) {
Task {
do {
// Get the paywall using the placement ID from the action
let paywall = try await Adapty.getPaywall(placementId: action.actionId)
// Get the paywall configuration
let paywallConfig = try await AdaptyUI.getPaywallConfiguration(
forPaywall: paywall
)
// Create and present the paywall controller
let paywallController = try AdaptyUI.paywallController(
with: paywallConfig,
delegate: self
)
// Present the paywall from the root view controller
if let rootVC = UIApplication.shared.windows.first?.rootViewController {
rootVC.present(paywallController, animated: true)
}
} catch {
// Handle any errors that occur during paywall loading
print("Failed to present paywall: \(error)")
}
}
}
}
```
Event example (Click to expand)
```json
{
"action_id": "premium_offer_1",
"meta": {
"onboarding_id": "onboarding_123",
"screen_cid": "pricing_screen",
"screen_index": 2,
"total_screens": 4
}
}
```
## Finishing loading onboarding
When an onboarding finishes loading, this method will be invoked:
```swift showLineNumbers
func onboardingController(_ controller: AdaptyOnboardingController, didFinishLoading action: OnboardingsDidFinishLoadingAction) {
// Handle loading completion
}
```
Event example (Click to expand)
```json
{
"meta": {
"onboarding_id": "onboarding_123",
"screen_cid": "welcome_screen",
"screen_index": 0,
"total_screens": 4
}
}
```
## Tracking navigation
The `onAnalyticsEvent` method is called when various analytics events occur during the onboarding flow.
The `event` object can be one of the following types:
|Type | Description |
|------------|-------------|
| `onboardingStarted` | When the onboarding has been loaded |
| `screenPresented` | When any screen is shown |
| `screenCompleted` | When a screen is completed. Includes optional `elementId` (identifier of the completed element) and optional `reply` (response from the user). Triggered when users perform any action to exit the screen. |
| `secondScreenPresented` | When the second screen is shown |
| `userEmailCollected` | Triggered when the user's email is collected via the input field |
| `onboardingCompleted` | Triggered when a user reaches a screen with the `final` ID. If you need this event, [assign the `final` ID to the last screen](design-onboarding.md). |
| `unknown` | For any unrecognized event type. Includes `name` (the name of the unknown event) and `meta` (additional metadata) |
Each event includes `meta` information containing:
| Field | Description |
|------------|-------------|
| `onboardingId` | Unique identifier of the onboarding flow |
| `screenClientId` | Identifier of the current screen |
| `screenIndex` | Current screen's position in the flow |
| `screensTotal` | Total number of screens in the flow |
Here's an example of how you can use analytics events for tracking:
```swift
func onboardingController(_ controller: AdaptyOnboardingController, onAnalyticsEvent event: AdaptyOnboardingsAnalyticsEvent) {
switch event {
case .onboardingStarted(let meta):
// Track onboarding start
trackEvent("onboarding_started", meta: meta)
case .screenPresented(let meta):
// Track screen presentation
trackEvent("screen_presented", meta: meta)
case .screenCompleted(let meta, let elementId, let reply):
// Track screen completion with user response
trackEvent("screen_completed", meta: meta, elementId: elementId, reply: reply)
case .onboardingCompleted(let meta):
// Track successful onboarding completion
trackEvent("onboarding_completed", meta: meta)
case .unknown(let meta, let name):
// Handle unknown events
trackEvent(name, meta: meta)
// Handle other cases as needed
}
}
```
Event examples (Click to expand)
```javascript
// onboardingStarted
{
"name": "onboarding_started",
"meta": {
"onboarding_id": "onboarding_123",
"screen_cid": "welcome_screen",
"screen_index": 0,
"total_screens": 4
}
}
// screenPresented
{
"name": "screen_presented",
"meta": {
"onboarding_id": "onboarding_123",
"screen_cid": "interests_screen",
"screen_index": 2,
"total_screens": 4
}
}
// screenCompleted
{
"name": "screen_completed",
"meta": {
"onboarding_id": "onboarding_123",
"screen_cid": "profile_screen",
"screen_index": 1,
"total_screens": 4
},
"params": {
"element_id": "profile_form",
"reply": "success"
}
}
// secondScreenPresented
{
"name": "second_screen_presented",
"meta": {
"onboarding_id": "onboarding_123",
"screen_cid": "profile_screen",
"screen_index": 1,
"total_screens": 4
}
}
// userEmailCollected
{
"name": "user_email_collected",
"meta": {
"onboarding_id": "onboarding_123",
"screen_cid": "profile_screen",
"screen_index": 1,
"total_screens": 4
}
}
// onboardingCompleted
{
"name": "onboarding_completed",
"meta": {
"onboarding_id": "onboarding_123",
"screen_cid": "final_screen",
"screen_index": 3,
"total_screens": 4
}
}
```
---
# File: ios-onboarding-input
---
---
title: "Process data from onboardings in iOS SDK"
description: "Save and use data from onboardings in your iOS app with Adapty SDK."
---
When your users respond to a quiz question or input their data into an input field, the `onStateUpdatedAction` method will be invoked. You can save or process the field type in your code.
For example:
```swift showLineNumbers
func onboardingController(_ controller: AdaptyOnboardingController, onStateUpdatedAction action: AdaptyOnboardingsStateUpdatedAction) {
// Store user preferences or responses
switch action.params {
case .select(let params):
// Handle single selection
case .multiSelect(let params):
// Handle multiple selections
case .input(let params):
// Handle text input
case .datePicker(let params):
// Handle date selection
}
}
```
The `action` object contains:
| Parameter | Description |
|----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `elementId` | A unique identifier for the input element. You can use it to associate questions with answers when saving them. |
| `params` | The user's input data object containing type and value properties. |
| `params.type` | The type of input element. Can be: • `"select"` - Single selection from options • `"multiSelect"` - Multiple selections from options • `"input"` - Text input field • `"datePicker"` - Date selection |
| `params.value` | The value(s) selected or entered by the user. Structure depends on type: • `select`: Object with `id`, `value`, `label` • `multiSelect`: Array of objects with `id`, `value`, `label` • `input`: Object with `type`, `value` • `datePicker`: Object with `day`, `month`, `year` |
Saved data examples (may differ in your implementation)
```javascript
// Example of a saved select action
{
"elementId": "preference_selector",
"meta": {
"onboardingId": "onboarding_123",
"screenClientId": "preferences_screen",
"screenIndex": 1,
"screensTotal": 3
},
"params": {
"type": "select",
"value": {
"id": "option_1",
"value": "premium",
"label": "Premium Plan"
}
}
}
// Example of a saved multi-select action
{
"elementId": "interests_selector",
"meta": {
"onboardingId": "onboarding_123",
"screenClientId": "interests_screen",
"screenIndex": 2,
"screensTotal": 3
},
"params": {
"type": "multiSelect",
"value": [
{
"id": "interest_1",
"value": "sports",
"label": "Sports"
},
{
"id": "interest_2",
"value": "music",
"label": "Music"
}
]
}
}
// Example of a saved input action
{
"elementId": "name_input",
"meta": {
"onboardingId": "onboarding_123",
"screenClientId": "profile_screen",
"screenIndex": 0,
"screensTotal": 3
},
"params": {
"type": "input",
"value": {
"type": "text",
"value": "John Doe"
}
}
}
// Example of a saved date picker action
{
"elementId": "birthday_picker",
"meta": {
"onboardingId": "onboarding_123",
"screenClientId": "profile_screen",
"screenIndex": 0,
"screensTotal": 3
},
"params": {
"type": "datePicker",
"value": {
"day": 15,
"month": 6,
"year": 1990
}
}
}
```
## Use cases
### Enrich user profiles with data
If you want to immediately link the input data with the user profile and avoid asking them twice for the same info, you need to [update the user profile](setting-user-attributes.md) with the input data when handling the action.
For example, you ask users to enter their name in the text field with the `name` ID, and you want to set this field's value as user's first name. Also, you ask them to enter their email in the `email` field. In your app code, it can look like this:
```swift showLineNumbers
func onboardingController(_ controller: AdaptyOnboardingController, onStateUpdatedAction action: AdaptyOnboardingsStateUpdatedAction) {
// Store user preferences or responses
switch action.params {
case .input(let params):
// Handle text input
let builder = AdaptyProfileParameters.Builder()
// Map elementId to appropriate profile field
switch action.elementId {
case "name":
builder.with(firstName: params.value.value)
case "email":
builder.with(email: params.value.value)
}
do {
try await Adapty.updateProfile(params: builder.build())
} catch {
// handle the error
}
}
```
### Customize paywalls based on answers
Using quizzes in onboardings, you can also customize paywalls you show users after they complete the onboarding.
For example, you can ask users about their experience with sport and show different CTAs and products to different user groups.
1. [Add a quiz](onboarding-quizzes.md) in the onboarding builder and assign meaningful IDs to its options.
2. Handle the quiz responses based on their IDs and [set custom attributes](setting-user-attributes.md) for users.
```swift showLineNumbers
func onboardingController(_ controller: AdaptyOnboardingController, onStateUpdatedAction action: AdaptyOnboardingsStateUpdatedAction) {
// Handle quiz responses and set custom attributes
switch action.params {
case .select(let params):
// Handle quiz selection
let builder = AdaptyProfileParameters.Builder()
// Map quiz responses to custom attributes
switch action.elementId {
case "experience":
// Set custom attribute 'experience' with the selected value (beginner, amateur, pro)
try? builder.with(customAttribute: params.value.value, forKey: "experience")
}
do {
try await Adapty.updateProfile(params: builder.build())
} catch {
// handle the error
}
}
}
```
3. [Create segments](segments.md) for each custom attribute value.
4. Create a [placement](placements.md) and add [audiences](audience.md) for each segment you've created.
5. [Display a paywall](ios-paywalls.md) for the placement in your app code. If your onboarding has a button that opens a paywall, implement the paywall code as a [response to this button's action](ios-handling-onboarding-events#opening-a-paywall).
---
# File: ios-test
---
---
title: "Test & release in iOS SDK"
description: "Learn how to check subscription status in your iOS app with Adapty."
---
If you've already implemented the Adapty SDK in your iOS app, you'll want to test that everything is set up correctly and that purchases work as expected. This involves testing both the SDK integration and the actual purchase.
## Test your app
For comprehensive testing of your in-app purchases, including sandbox testing and TestFlight validation, see our [testing guide](test-purchases-in-sandbox.md).
## Prepare for release
Before submitting your app to the store, follow the [Release checklist](release-checklist) to confirm:
- Store connection and server notifications are configured
- Purchases complete and are reported to Adapty
- Access unlocks and restores correctly
- Privacy and review requirements are met
---
# File: InvalidProductIdentifiers
---
---
title: "Fix for Code-1000 noProductIDsFound error"
description: "Resolve invalid product identifier errors when managing subscriptions in Adapty."
---
The 1000-code error, `noProductIDsFound`, indicates that none of the products you requested on the paywall are available for purchase in the App Store, even though they’re listed there. This error may sometimes come with an `InvalidProductIdentifiers` warning. If the warning appears without an error, safely ignore it.
If you’re encountering the `noProductIDsFound` error, follow these steps to resolve it:
## Step 1. Check bundle ID \{#step-2-check-bundle-id\}
---
no_index: true
---
1. Open [App Store Connect](https://appstoreconnect.apple.com/apps). Select your app and proceed to **General** → **App Information** section.
2. Copy the **Bundle ID** in the **General Information** sub-section.
3. Open the [**App settings** -> **iOS SDK** tab](https://app.adapty.io/settings/ios-sdk) from the Adapty top menu and paste the copied value to the **Bundle ID** field.
4. Get back to the **App information** page in App Store Connect and copy **Apple ID** from there.
5. On the [**App settings** -> **iOS SDK**](https://app.adapty.io/settings/ios-sdk) page in the Adapty dashboard, paste the ID to the **Apple app ID** field.
## Step 2. Check products \{#step-3-check-products\}
1. Go to **App Store Connect** and navigate to [**Monetization** → **Subscriptions**](https://appstoreconnect.apple.com/apps/6477523342/distribution/subscriptions) in the left-hand menu.
2. Click on the subscription group name. You’ll see your products listed under the **Subscriptions** section.
3. Ensure the product you're testing is marked **Ready to Submit**. If not, follow the instructions on the [Product in App Store](app-store-products) page.
4. Compare the product ID from the table with the one in the [**Products**](https://app.adapty.io/products) tab in the Adapty Dashboard. If the IDs don’t match, copy the product ID from the table and [create a product](create-product) with it in the Adapty Dashboard.
## Step 3. Check product availability \{#step-4-check-product-availability\}
1. Go back to **App Store Connect** and open the same **Subscriptions** section.
2. Click the subscription group name to view your products.
3. Select the product you're testing.
4. Scroll to the **Availability** section and check that all the required countries and regions are listed.
## Step 4. Check product prices \{#step-5-check-product-prices\}
1. Again, head to the **Monetization** → **Subscriptions** section in **App Store Connect**.
2. Click the subscription group name.
3. Select the product you’re testing.
4. Scroll down to **Subscription Pricing** and expand the **Current Pricing for New Subscribers** section.
5. Ensure that all required prices are listed.
## Step 5. Check app paid status, bank account, and tax forms are active
1. In **App Store Connect**](https://appstoreconnect.apple.com/) homepage, click **Business**.
2. Select your company name.
3. Scroll down and check that your **Paid Apps Agreement**, **Bank Account**, and **Tax forms** all show as **Active**.
By following these steps, you should be able to resolve the `InvalidProductIdentifiers` warning and get your products live in the store
---
# File: cantMakePayments
---
---
title: "Fix for Code-1003 cantMakePayment error"
description: "Resolve making payments error when managing subscriptions in Adapty."
---
The 1003 error, `cantMakePayments`, indicates that in-app purchases can't be made on this device.
If you’re encountering the `cantMakePayments` error, this is usually due to one of the reasons:
- Device restrictions: The error is not related to Adapty. See the ways to fix the issue below.
- Observer mode configuration: The `makePurchase` method and the observer mode can't be used at the same time. See the section below.
## Issue: Device restrictions
| Issue | Solution |
|---------------------------|---------------------------------------------------------|
| Screen Time restrictions | Disable In-App Purchase restrictions in [Screen Time](https://support.apple.com/en-us/102470) |
| Account suspended | Contact Apple Support to resolve account issues |
| Regional restrictions | Use App Store account from supported region |
## Issue: Using both Observer mode and makePurchase
If you are using `makePurchases` to handle purchases, you don't need to use Observer mode. [Observer mode](https://adapty.io/docs/observer-vs-full-mode) is only needed if you implement the purchase logic yourself.
So, if you're using `makePurchase`, you can safely remove enabling Observer mode from the SDK activation code.
---
# File: migration-to-ios-315
---
---
title: "Migrate Adapty iOS SDK to v. 3.15"
description: "Migrate to Adapty iOS SDK v3.15 for better performance and new monetization features."
---
If you use [Paywall Builder](adapty-paywall-builder.md) in [Observer mode](observer-vs-full-mode), starting from iOS SDK 3.15, you need to implement a new method `observerModeDidInitiateRestorePurchases(onStartRestore:onFinishRestore:)`. This method provides more control over the restore logic, allowing you to handle restore purchases in your custom flow. For complete implementation details, refer to [Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode).
```diff showLineNumbers
func observerMode(didInitiatePurchase product: AdaptyPaywallProduct,
onStartPurchase: @escaping () -> Void,
onFinishPurchase: @escaping () -> Void) {
// use the product object to handle the purchase
// use the onStartPurchase and onFinishPurchase callbacks to notify AdaptyUI about the process of the purchase
}
+ func observerModeDidInitiateRestorePurchases(onStartRestore: @escaping () -> Void,
+ onFinishRestore: @escaping () -> Void) {
+ // use the onStartRestore and onFinishRestore callbacks to notify AdaptyUI about the process of the restore
+ }
```
---
# File: migration-to-ios-sdk-34
---
---
title: "Migrate Adapty iOS SDK to v. 3.4"
description: "Migrate to Adapty iOS SDK v3.4 for better performance and new monetization features."
---
Adapty SDK 3.4.0 is a major release that introduces improvements that require migration steps on your end.
## Update Adapty SDK activation
```diff showLineNumbers
// In your AppDelegate class:
let configurationBuilder =
AdaptyConfiguration
.builder(withAPIKey: "PUBLIC_SDK_KEY")
- Adapty.activate(with: configurationBuilder) { error in
+ Adapty.activate(with: configurationBuilder.build()) { error in
// handle the error
}
```
**Update fallback paywall files**
Update your fallback paywall files to ensure compatibility with the new SDK version:
1. [Download the updated fallback paywall files](fallback-paywalls#download-fallback-paywalls-as-a-file-in-the-adapty-dashboard) from the Adapty Dashboard.
2. [Replace the existing fallback paywalls in your mobile app](ios-use-fallback-paywalls) with the new files.
```diff showLineNumbers
@main
struct SampleApp: App {
init() {
let configurationBuilder =
AdaptyConfiguration
.builder(withAPIKey: "PUBLIC_SDK_KEY")
Task {
- try await Adapty.activate(with: configurationBuilder)
+ try await Adapty.activate(with: configurationBuilder.build())
}
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
```
**Update fallback paywall files**
Update your fallback paywall files to ensure compatibility with the new SDK version:
1. [Download the updated fallback paywall files](fallback-paywalls#download-fallback-paywalls-as-a-file-in-the-adapty-dashboard) from the Adapty Dashboard.
2. [Replace the existing fallback paywalls in your mobile app](ios-use-fallback-paywalls) with the new files.
---
# File: migration-to-ios330
---
---
title: "Migrate Adapty iOS SDK to v. 3.3"
description: "Migrate to Adapty iOS SDK v3.3 for better performance and new monetization features."
---
Adapty SDK 3.3.0 is a major release that brought some improvements which however may require some migration steps from you.
1. Rename `Adapty.Configuration` to `AdaptyConfiguration`.
2. Rename the `getViewConfiguration` method to `getPaywallConfiguration`.
3. Remove the `didCancelPurchase` and `paywall` parameters from SwiftUI, and rename the `viewConfiguration` parameter to `paywallConfiguration`.
4. Update how you process promotional in-app purchases from the App Store by removing the `defermentCompletion` parameter from the `AdaptyDelegate` method.
5. Remove the `getProductsIntroductoryOfferEligibility` method.
6. Update integration configurations for Adjust, AirBridge, Amplitude, AppMetrica, Appsflyer, Branch, Facebook Ads, Firebase and Google Analytics, Mixpanel, OneSignal, Pushwoosh.
7. Update Observer mode implementation.
## Rename Adapty.Configuration to AdaptyConfiguration
Update the code of Adapty iOS SDK activation in the following way:
```diff showLineNumbers
// In your AppDelegate class:
let configurationBuilder =
- Adapty.Configuration
+ AdaptyConfiguration
.builder(withAPIKey: "PUBLIC_SDK_KEY")
.with(observerMode: false)
.with(customerUserId: "YOUR_USER_ID")
.with(idfaCollectionDisabled: false)
.with(ipAddressCollectionDisabled: false)
Adapty.activate(with: configurationBuilder) { error in
// handle the error
}
```
```diff showLineNumbers
@main
struct SampleApp: App {
init()
let configurationBuilder =
- Adapty.Configuration
+ AdaptyConfiguration
.builder(withAPIKey: "PUBLIC_SDK_KEY")
.with(observerMode: false) // optional
.with(customerUserId: "YOUR_USER_ID") // optional
.with(idfaCollectionDisabled: false) // optional
.with(ipAddressCollectionDisabled: false) // optional
Task {
try await Adapty.activate(with: configurationBuilder)
}
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
```
## Rename getViewConfiguration method to getPaywallConfiguration
Update the method name to fetch the paywall's `viewConfiguration`:
```diff showLineNumbers
guard paywall.hasViewConfiguration else {
// use your custom logic
return
}
do {
- let paywallConfiguration = try await AdaptyUI.getViewConfiguration(
+ let paywallConfiguration = try await AdaptyUI.getPaywallConfiguration(
forPaywall: paywall
)
// use loaded configuration
} catch {
// handle the error
}
```
For more details about the method, check out [Fetch the view configuration of paywall designed using Paywall Builder](get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder).
## Change parameters in SwiftUI
The following updates have been made to SwiftUI:
1. The `didCancelPurchase` parameter has been removed. Use `didFinishPurchase` instead.
2. The `.paywall()` method no longer accepts a paywall object.
3. The `paywallConfiguration` parameter has replaced the `viewConfiguration` parameter.
Update your code like this:
```diff showLineNumbers
@State var paywallPresented = false
var body: some View {
Text("Hello, AdaptyUI!")
.paywall(
isPresented: $paywallPresented,
- paywall: ,
- viewConfiguration: ,
+ paywallConfiguration: ,
didPerformAction: { action in
switch action {
case .close:
paywallPresented = false
default:
// Handle other actions
break
}
},
- didFinishPurchase: { product, profile in paywallPresented = false },
+ didFinishPurchase: { product, purchaseResult in /* handle the result*/ },
didFailPurchase: { product, error in /* handle the error */ },
didFinishRestore: { profile in /* check access level and dismiss */ },
didFailRestore: { error in /* handle the error */ },
didFailRendering: { error in paywallPresented = false }
- didCancelPurchase: { product in /* handle the result*/}
)
}
```
## Update handling of promotional in-app purchases from App Store
Update how you handle promotional in-app purchases from the App Store by removing the `defermentCompletion` parameter from the `AdaptyDelegate` method, as shown in the example below:
```swift showLineNumbers title="Swift"
final class YourAdaptyDelegateImplementation: AdaptyDelegate {
nonisolated func shouldAddStorePayment(for product: AdaptyDeferredProduct) -> Bool {
// 1a.
// Return `true` to continue the transaction in your app.
// 1b.
// Store the product object and return `false` to defer or cancel the transaction.
false
}
// 2. Continue the deferred purchase later on by passing the product to `makePurchase`
func continueDeferredPurchase() async {
let storedProduct: AdaptyDeferredProduct = // get the product object from the 1b.
do {
try await Adapty.makePurchase(product: storedProduct)
} catch {
// handle the error
}
}
}
```
## Remove getProductsIntroductoryOfferEligibility method
Before Adapty iOS SDK 3.3.0, the product object always included offers, regardless of whether the user was eligible. You had to manually check eligibility before using the offer.
Now, the product object only includes an offer if the user is eligible. This means you no longer need to check eligibility — if an offer is present, the user is eligible.
If you still want to view offers for users who are not eligible, refer to `sk1Product` and `sk2Product`.
## Update third-party integration SDK configuration
Starting with Adapty iOS SDK 3.3.0, we’ve updated the public API for the `updateAttribution` method. Previously, it accepted a `[AnyHashable: Any]` dictionary, allowing you to pass attribution objects directly from various services. Now, it requires a `[String: any Sendable]`, so you’ll need to convert attribution objects before passing them.
To ensure integrations work properly with Adapty iOS SDK 3.3.0 and later, update your SDK configurations for the following integrations as described in the sections below.
### Adjust
Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Adjust integration](adjust#sdk-configuration).
```diff showLineNumbers
class AdjustModuleImplementation {
- func updateAdjustAttribution() {
- Adjust.attribution { attribution in
- guard let attributionDictionary = attribution?.dictionary()?.toSendableDict() else { return }
-
- Adjust.adid { adid in
- guard let adid else { return }
-
- Adapty.updateAttribution(attributionDictionary, source: .adjust, networkUserId: adid) { error in
- // handle the error
- }
- }
- }
- }
+ func updateAdjustAdid() {
+ Adjust.adid { adid in
+ guard let adid else { return }
+
+ Adapty.setIntegrationIdentifier(key: "adjust_device_id", value: adid)
+ }
+ }
+
+ func updateAdjustAttribution() {
+ Adjust.attribution { attribution in
+ guard let attribution = attribution?.dictionary() else {
+ return
+ }
+
+ Adapty.updateAttribution(attribution, source: "adjust")
+ }
+ }
}
```
```diff showLineNumbers
class YourAdjustDelegateImplementation {
// Find your implementation of AdjustDelegate
// and update adjustAttributionChanged method:
func adjustAttributionChanged(_ attribution: ADJAttribution?) {
- if let attribution = attribution?.dictionary()?.toSendableDict() {
- Adapty.updateAttribution(attribution, source: .adjust)
+ if let attribution = attribution?.dictionary() {
+ Adapty.updateAttribution(attribution, source: "adjust")
}
}
}
```
### AirBridge
Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for AirBridge integration](airbridge#sdk-configuration).
```diff showLineNumbers
import AirBridge
- let builder = AdaptyProfileParameters.Builder()
- .with(airbridgeDeviceId: AirBridge.deviceUUID())
-
- Adapty.updateProfile(params: builder.build())
+ do {
+ try await Adapty.setIntegrationIdentifier(
+ key: "airbridge_device_id",
+ value: AirBridge.deviceUUID()
+ )
+ } catch {
+ // handle the error
+ }
```
### Amplitude
Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Amplitude integration](amplitude#sdk-configuration).
```diff showLineNumbers
import Amplitude
- let builder = AdaptyProfileParameters.Builder()
- .with(amplitudeUserId: Amplitude.instance().userId)
- .with(amplitudeDeviceId: Amplitude.instance().deviceId)
-
- Adapty.updateProfile(params: builder.build())
+ do {
+ try await Adapty.setIntegrationIdentifier(
+ key: "amplitude_user_id",
+ value: Amplitude.instance().userId
+ )
+ try await Adapty.setIntegrationIdentifier(
+ key: "amplitude_device_id",
+ value: Amplitude.instance().deviceId
+ )
+ } catch {
+ // handle the error
+ }
```
### AppMetrica
Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for AppMetrica integration](appmetrica#sdk-configuration).
```diff showLineNumbers
import AppMetricaCore
- if let deviceID = AppMetrica.deviceID {
- let builder = AdaptyProfileParameters.Builder()
- .with(appmetricaDeviceId: deviceID)
- .with(appmetricaProfileId: "YOUR_ADAPTY_CUSTOMER_USER_ID")
-
- Adapty.updateProfile(params: builder.build())
- }
+ if let deviceID = AppMetrica.deviceID {
+ do {
+ try await Adapty.setIntegrationIdentifier(
+ key: "appmetrica_device_id",
+ value: deviceID
+ )
+ try await Adapty.setIntegrationIdentifier(
+ key: "appmetrica_profile_id",
+ value: "YOUR_ADAPTY_CUSTOMER_USER_ID"
+ )
+ } catch {
+ // handle the error
+ }
+ }
```
### AppsFlyer
Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for AppsFlyer integration](appsflyer#sdk-configuration).
```diff showLineNumbers
class YourAppsFlyerLibDelegateImplementation {
// Find your implementation of AppsFlyerLibDelegate
// and update onConversionDataSuccess method:
func onConversionDataSuccess(_ conversionInfo: [AnyHashable : Any]) {
let uid = AppsFlyerLib.shared().getAppsFlyerUID()
- Adapty.updateAttribution(
- conversionInfo.toSendableDict(),
- source: .appsflyer,
- networkUserId: uid
- )
+ Adapty.setIntegrationIdentifier(key: "appsflyer_id", value: uid)
+ Adapty.updateAttribution(conversionInfo, source: "appsflyer")
}
}
```
### Branch
Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Branch integration](branch#sdk-configuration).
```diff showLineNumbers
class YourBranchImplementation {
func initializeBranch() {
// Pass the attribution you receive from the initializing method of Branch iOS SDK to Adapty.
Branch.getInstance().initSession(launchOptions: launchOptions) { (data, error) in
- if let data = data?.toSendableDict() {
- Adapty.updateAttribution(data, source: .branch)
- }
+ if let data {
+ Adapty.updateAttribution(data, source: "branch")
+ }
}
}
}
```
### Facebook Ads
Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Facebook Ads integration](facebook-ads#sdk-configuration).
```diff showLineNumbers
import FacebookCore
- let builder = AdaptyProfileParameters.Builder()
- .with(facebookAnonymousId: AppEvents.shared.anonymousID)
-
- do {
- try Adapty.updateProfile(params: builder.build())
- } catch {
- // handle the error
- }
+ do {
+ try await Adapty.setIntegrationIdentifier(
+ key: "facebook_anonymous_id",
+ value: AppEvents.shared.anonymousID
+ )
+ } catch {
+ // handle the error
+ }
```
### Firebase and Google Analytics
Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Firebase and Google Analytics integration](firebase-and-google-analytics).
```diff showLineNumbers
import FirebaseCore
import FirebaseAnalytics
FirebaseApp.configure()
- if let appInstanceId = Analytics.appInstanceID() {
- let builder = AdaptyProfileParameters.Builder()
- .with(firebaseAppInstanceId: appInstanceId)
- Adapty.updateProfile(params: builder.build()) { error in
- // handle error
- }
- }
+ if let appInstanceId = Analytics.appInstanceID() {
+ do {
+ try await Adapty.setIntegrationIdentifier(
+ key: "firebase_app_instance_id",
+ value: appInstanceId
+ )
+ } catch {
+ // handle the error
+ }
+ }
```
### Mixpanel
Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Mixpanel integration](mixpanel#sdk-configuration).
```diff showLineNumbers
import Mixpanel
- let builder = AdaptyProfileParameters.Builder()
- .with(mixpanelUserId: Mixpanel.mainInstance().distinctId)
-
- do {
- try await Adapty.updateProfile(params: builder.build())
- } catch {
- // handle the error
- }
+ do {
+ try await Adapty.setIntegrationIdentifier(
+ key: "mixpanel_user_id",
+ value: Mixpanel.mainInstance().distinctId
+ )
+ } catch {
+ // handle the error
+ }
```
### OneSignal
Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for OneSignal integration](onesignal#sdk-configuration).
```diff showLineNumbers
// PlayerID (pre-v5 OneSignal SDK)
// in your OSSubscriptionObserver implementation
func onOSSubscriptionChanged(_ stateChanges: OSSubscriptionStateChanges) {
if let playerId = stateChanges.to.userId {
- let params = AdaptyProfileParameters.Builder()
- .with(oneSignalPlayerId: playerId)
- .build()
-
- Adapty.updateProfile(params:params) { error in
- // check error
- }
+ Task {
+ try await Adapty.setIntegrationIdentifier(
+ key: "one_signal_player_id",
+ value: playerId
+ )
+ }
}
}
// SubscriptionID (v5+ OneSignal SDK)
OneSignal.Notifications.requestPermission({ accepted in
- let id = OneSignal.User.pushSubscription.id
-
- let builder = AdaptyProfileParameters.Builder()
- .with(oneSignalSubscriptionId: id)
-
- Adapty.updateProfile(params: builder.build())
+ Task {
+ try await Adapty.setIntegrationIdentifier(
+ key: "one_signal_subscription_id",
+ value: OneSignal.User.pushSubscription.id
+ )
+ }
}, fallbackToSettings: true)
```
### Pushwoosh
Update your mobile app code as shown below. For the complete code example, check out the [SDK configuration for Pushwoosh integration](pushwoosh#sdk-configuration).
```diff showLineNumbers
- let params = AdaptyProfileParameters.Builder()
- .with(pushwooshHWID: Pushwoosh.sharedInstance().getHWID())
- .build()
-
- Adapty.updateProfile(params: params) { error in
- // handle the error
- }
+ do {
+ try await Adapty.setIntegrationIdentifier(
+ key: "pushwoosh_hwid",
+ value: Pushwoosh.sharedInstance().getHWID()
+ )
+ } catch {
+ // handle the error
+ }
```
## Update Observer mode implementation
Update how you link paywalls to transactions. Previously, you used the `setVariationId` method to assign the `variationId`. Now, you can include the `variationId` directly when recording the transaction using the new `reportTransaction` method. Check out the final code example in the [Associate paywalls with purchase transactions in Observer mode](report-transactions-observer-mode).
:::warning
Remember to record the transaction using the `reportTransaction` method. Skipping this step means Adapty won't recognize the transaction, grant access levels, include it in analytics, or send it to integrations. This step is essential!
:::
```diff showLineNumbers
- let variationId = paywall.variationId
-
- // There are two overloads: for StoreKit 1 and StoreKit 2
- Adapty.setVariationId(variationId, forPurchasedTransaction: transaction) { error in
- if error == nil {
- // successful binding
- }
- }
+ do {
+ // every time when calling transaction.finish()
+ try await Adapty.reportTransaction(transaction, withVariationId: )
+ } catch {
+ // handle the error
+ }
```
---
# File: migration-to-ios-sdk-v3
---
---
title: "Migrate Adapty iOS SDK to v. 3.0"
description: "Migrate to Adapty iOS SDK v3.0 for better performance and new monetization features."
---
Adapty SDK v.3.0 brings support for the new exciting [Adapty Paywall Builder](adapty-paywall-builder), the new version of the no-code user-friendly tool to create paywalls. With its maximum flexibility and rich design capabilities, your paywalls will become most effective and profitable.
:::info
Please note that the AdaptyUI library is deprecated and is now included as part of AdaptySDK.
:::
## Reinstall Adapty SDK v3.x via Swift Package Manager
1. Delete AdaptyUI SDK package dependency from your project, you won't need it anymore.
2. Even though you have it already, you'll need to re-add the Adapty SDK dependency. For this, in Xcode, open **File** -> **Add Package Dependency...**. Please note the way to add package dependencies can differ in XCode versions. Refer to XCode documentation if necessary.
3. Enter the repository URL `https://github.com/adaptyteam/AdaptySDK-iOS.git`
4. Choose the version, and click the **Add package** button.
5. Choose the modules you need:
1. **Adapty** is the mandatory module
2. **AdaptyUI** is an optional module you need if you plan to use the [Adapty Paywall Builder](adapty-paywall-builder).
6. Xcode will add the package dependency to your project, and you can import it. For this, in the **Choose Package Products** window, click the **Add package** button once again. The package will appear in the **Packages** list.
## Reinstall Adapty SDK v3.x via CocoaPods
1. Add Adapty to your `Podfile`. Choose the modules you need:
1. **Adapty** is the mandatory module.
2. **AdaptyUI** is an optional module you need if you plan to use the [Adapty Paywall Builder](adapty-paywall-builder).
2. ```shell showLineNumbers title="Podfile"
pod 'Adapty', '~> 3.2.0'
pod 'AdaptyUI', '~> 3.2.0' # optional module needed only for Paywall Builder
```
3. Run:
```sh showLineNumbers title="Shell"
pod install
```
This creates a `.xcworkspace` file for your app. Use this file for all future development of your application.
Activate Adapty and AdaptyUI SDK modules. Before v3.0, you did not activate AdaptyUI, remember to **add AdaptyUI activation**. Parameters are not changes, so keep them as is.
```swift showLineNumbers
// In your AppDelegate class:
let configurationBuilder =
AdaptyConfiguration
.Builder(withAPIKey: "PUBLIC_SDK_KEY")
.with(observerMode: false)
.with(customerUserId: "YOUR_USER_ID")
.with(idfaCollectionDisabled: false)
.with(ipAddressCollectionDisabled: false)
Adapty.activate(with: configurationBuilder) { error in
// handle the error
}
// Only if you are going to use AdaptyUI
AdaptyUI.activate()
```
```swift title="" showLineNumbers
@main
struct SampleApp: App {
init()
let configurationBuilder =
AdaptyConfiguration
.Builder(withAPIKey: "PUBLIC_SDK_KEY")
.with(observerMode: false) // optional
.with(customerUserId: "YOUR_USER_ID") // optional
.with(idfaCollectionDisabled: false) // optional
.with(ipAddressCollectionDisabled: false) // optional
Adapty.activate(with: configurationBuilder) { error in
// handle the error
}
// Only if you are going to use AdaptyUI
AdaptyUI.activate()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
```
---
# File: ios-legacy-install
---
---
title: "Legacy installation guide"
description: "Get started with Adapty on iOS to streamline subscription setup and management."
---
Please consult the compatibility table below to choose the correct pair of Adapty SDK and AdaptyUI SDK.
| Adapty SDK version | AdaptyUI SDK version |
| :----------------------------------- | :------------------- |
| 2.7.x, 2.8.x | 2.0.x |
| 2.9.x - 2.10.0 | 2.1.2 |
| 2.10.1 | 2.1.3 |
| 2.10.3 and all later 2.10.x versions | 2.1.5 |
| 2.11.1 | 2.11.1 |
| 2.11.2 | 2.11.2 |
| 2.11.3 | 2.11.3 |
You can install AdaptySDK and AdaptyUI SDK via CocoaPods, or Swift Package Manager.
:::danger
Go through release checklist before releasing your app
Before releasing your application, make sure to carefully review the [Release Checklist](release-checklist) thoroughly. This checklist ensures that you've completed all necessary steps and provides criteria for evaluating the success of your integration.
:::
## Install SDKs via Swift Package Manager
1. In Xcode go to **File** -> **Add Package Dependency...**. Please note the way to add package dependencies can differ in XCode versions. Refer to XCode documentation if necessary.
2. Enter the repository URL `https://github.com/adaptyteam/AdaptySDK-iOS.git`
3. Choose the version, and click the **Add package** button. Xcode will add the package dependency to your project, and you can import it.
4. In the **Choose Package Products** window, click the **Add package** button once again. The package will appear in the **Packages** list.
5. Repeat steps 2-3 for AdaptyUI SDK URL: `https://github.com/adaptyteam/AdaptyUI-iOS.git`.
## Install SDKs via CocoaPods
:::info
CocoaPods is now in maintenance mode, with development officially stopped. We recommend switching to [Swift Package Manager](sdk-installation-ios#install-adapty-sdk-via-swift-package-manager).
:::
1. Add Adapty to your `Podfile`:
```shell showLineNumbers title="Podfile"
pod 'Adapty', '~> 2.11.3'
pod 'AdaptyUI', '~> 2.11.3'
```
2. Run:
```sh showLineNumbers title="Shell"
pod install
```
This creates a `.xcworkspace` file for your app. Use this file for all future development of your application.
## Configure Adapty SDK
You only need to configure the Adapty SDK once, typically early in your application lifecycle:
```swift showLineNumbers
// In your AppDelegate class:
let configurationBuilder =
AdaptyConfiguration
.Builder(withAPIKey: "PUBLIC_SDK_KEY")
.with(observerMode: false) // optional
.with(customerUserId: "YOUR_USER_ID") // optional
.with(idfaCollectionDisabled: false) // optional
.with(ipAddressCollectionDisabled: false) // optional
Adapty.activate(with: configurationBuilder.build()) { error in
// handle the error
}
```
```swift showLineNumbers
@main
struct SampleApp: App {
init()
let configurationBuilder =
AdaptyConfiguration
.Builder(withAPIKey: "PUBLIC_SDK_KEY")
.with(observerMode: false) // optional
.with(customerUserId: "YOUR_USER_ID") // optional
.with(idfaCollectionDisabled: false) // optional
.with(ipAddressCollectionDisabled: false) // optional
.with(LogLevel: verbose) // optional
Adapty.activate(with: configurationBuilder.build()) { error in
// handle the error
}
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
```
Parameters:
| Parameter | Presence | Description |
| --------------------------- | -------- | ------------------------------------------------------------ |
| apiKey | required | The key you can find in the **Public SDK key** field of your app settings in Adapty: [**App settings**-> **General** tab -> **API keys** subsection](https://app.adapty.io/settings/general) |
| observerMode | optional |
A boolean value controlling [Observer mode](observer-vs-full-mode). Turn it on if you handle purchases and subscription status yourself and use Adapty for sending subscription events and analytics.
The default value is `false`.
🚧 When running in Observer mode, Adapty SDK won't close any transactions, so make sure you're handling it.
|
| customerUserId | optional | An identifier of the user in your system. We send it in subscription and analytical events, to attribute events to the right profile. You can also find customers by `customerUserId` in the [**Profiles and Segments**](https://app.adapty.io/profiles/users) menu. |
| idfaCollectionDisabled | optional |
Set to `true` to disable IDFA collection and sharing.
the user IP address sharing.
The default value is `false`.
For more details on IDFA collection, refer to the [Analytics integration](analytics-integration#disable-collection-of-advertising-identifiers) section.
|
| ipAddressCollectionDisabled | optional |
Set to `true` to disable user IP address collection and sharing.
The default value is `false`.
|
| logLevel | optional | Adapty logs errors and other crucial information to provide insight into your app's functionality. There are the following available levels:
error: Only errors will be logged.
warn: Errors and messages from the SDK that do not cause critical errors, but are worth paying attention to will be logged.
info: Errors, warnings, and serious information messages, such as those that log the lifecycle of various modules will be logged.
verbose: Any additional information that may be useful during debugging, such as function calls, API queries, etc. will be logged.
|
:::note
- Note, that StoreKit 2 is available since iOS 15.0. Adapty will implement the legacy logic for older versions.
- Make sure you use the **Public SDK key** for Adapty initialization, the **Secret key** should be used for [server-side API](getting-started-with-server-side-api) only.
- **SDK keys** are unique for every app, so if you have multiple apps make sure you choose the right one.
:::
Please keep in mind that for paywalls and products to be displayed in your mobile application, and for analytics to work, you need to [display the paywalls](ios-present-paywalls) and, if you're using paywalls not created with the Paywall Builder, [handle the purchase process](making-purchases) within your app.
---
# File: ios-get-legacy-pb-paywalls
---
---
title: "Fetch legacy Paywall Builder paywalls in iOS SDK"
description: "Retrieve legacy PB paywalls in your iOS app with Adapty SDK."
---
After [you designed the visual part for your paywall](adapty-paywall-builder-legacy) with Paywall Builder in the Adapty Dashboard, you can display it in your iOS app. The first step in this process is to get the paywall associated with the placement and its view configuration as described below.
:::warning
This guide is for **legacy Paywall Builder paywalls** only which require SDK v2.x or earlier. The process for fetching paywalls differs for paywalls designed with different versions of Paywall Builder and remote config paywalls.
- For fetching **New Paywall Builder paywalls**, check out [Fetch new Paywall Builder paywalls and their configuration](get-pb-paywalls).
- For fetching **Remote config paywalls**, see [Fetch paywalls and products for remote config paywalls](fetch-paywalls-and-products).
:::
Before you start displaying paywalls in your iOS app (click to expand)
1. [Create your products](create-product) in the Adapty Dashboard.
2. [Create a paywall and incorporate the products into it](create-paywall) in the Adapty Dashboard.
3. [Create placements and incorporate your paywall into it](create-placement) in the Adapty Dashboard.
4. [Install Adapty SDK and AdaptyUI DSK](sdk-installation-ios) in your iOS app.
## Fetch paywall designed with Paywall Builder
If you've [designed a paywall using the Paywall Builder](adapty-paywall-builder-legacy), you don't need to worry about rendering it in your iOS app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown. Nevertheless, you need to get its ID via the placement, its view configuration, and then present it in your iOS app.
To ensure optimal performance, it's crucial to retrieve the paywall and its [view configuration](#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) as early as possible, allowing sufficient time for images to download before presenting them to the user.
To get a paywall, use the `getPaywall` method:
```swift showLineNumbers
Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en") { result in
switch result {
case let .success(paywall):
// the requested paywall
case let .failure(error):
// handle the error
}
}
```
| Parameter | Presence | Description |
|---------|--------|-----------|
| **placementId** | required | The identifier of the desired [Placement](placements). This is the value you specified when creating a placement in the Adapty Dashboard. |
| **locale** |
optional
default: `en`
|
The identifier of the [paywall localization](add-paywall-locale-in-adapty-paywall-builder). This parameter is expected to be a language code composed of one or two subtags separated by the minus (**-**) character. The first subtag is for the language, the second one is for the region.
Example: `en` means English, `pt-br` represents the Brazilian Portuguese language.
See [Localizations and locale codes](localizations-and-locale-codes) for more information on locale codes and how we recommend using them.
By default, SDK will try to load data from the server and will return cached data in case of failure. We recommend this variant because it ensures your users always get the most up-to-date data.
However, if you believe your users deal with unstable internet, consider using `.returnCacheDataElseLoad` to return cached data if it exists. In this scenario, users might not get the absolute latest data, but they'll experience faster loading times, no matter how patchy their internet connection is. The cache is updated regularly, so it's safe to use it during the session to avoid network requests.
Note that the cache remains intact upon restarting the app and is only cleared when the app is reinstalled or through manual cleanup.
Adapty SDK stores paywalls locally in two layers: regularly updated cache described above and [fallback paywalls](fallback-paywalls). We also use CDN to fetch paywalls faster and a stand-alone fallback server in case the CDN is unreachable. This system is designed to make sure you always get the latest version of your paywalls while ensuring reliability even in cases where internet connection is scarce.
|
**Don't hardcode product IDs.** The only ID you should hardcode is the placement ID. Paywalls are configured remotely, so the number of products and available offers can change at any time. Your app must handle these changes dynamically—if a paywall returns two products today and three tomorrow, display all of them without code changes.
Response parameters:
| Parameter | Description |
| :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Paywall | An [`AdaptyPaywall`](https://swift.adapty.io/documentation/adapty/adaptypaywall) object with a list of product IDs, the paywall identifier, remote config, and several other properties. |
## Fetch the view configuration of paywall designed using Paywall Builder
After fetching the paywall, check if it includes a `viewConfiguration`, which indicates that it was created using Paywall Builder. This will guide you on how to display the paywall. If the `viewConfiguration` is present, treat it as a Paywall Builder paywall; if not, [handle it as a remote config paywall](present-remote-config-paywalls).
Use the `getViewConfiguration` method to load the view configuration.
```swift showLineNumbers
do {
guard paywall.hasViewConfiguration else {
// use your custom logic
return
}
let paywallConfiguration = try await AdaptyUI.getPaywallConfiguration(forPaywall: paywall)
// use loaded configuration
} catch {
// handle the error
}
```
---
# File: ios-present-paywalls-legacy
---
---
title: "Present legacy Paywall Builder paywalls in iOS SDK"
description: "Discover how to present paywalls in iOS using Adapty’s legacy methods."
---
If you've customized a paywall using the Paywall Builder, you don't need to worry about rendering it in your mobile app code to display it to the user. Such a paywall contains both what should be shown within the paywall and how it should be shown.
:::warning
This guide is for **legacy Paywall Builder paywalls** only which require SDK v2.x or earlier. The process for presenting paywalls differs for paywalls designed with different versions of Paywall Builde, remote config paywalls, and [Observer mode](observer-vs-full-mode).
- For presenting **New Paywall Builder paywalls**, check out [iOS - Present new Paywall Builder paywalls](ios-present-paywalls).
- For presenting **Remote config paywalls**, see [Render paywall designed by remote config](present-remote-config-paywalls).
- For presenting **Observer mode paywalls**, see [iOS - Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode)
:::
## Present paywalls in Swift
In order to display the visual paywall on the device screen, you must first configure it. To do this, use the method `.paywallController(for:products:viewConfiguration:delegate:)`:
```swift showLineNumbers title="Swift"
let visualPaywall = AdaptyUI.paywallController(
for: ,
products: ,
viewConfiguration: ,
delegate:
)
```
Request parameters:
| Parameter | Presence | Description |
| :-------------------- | :------- | :----------------------------------------------------------- |
| **Paywall** | required | An `AdaptyPaywall` object to obtain a controller for the desired paywall. |
| **Products** | optional | Provide an array of `AdaptyPaywallProducts` to optimize the display timing of products on the screen. If `nil` is passed, AdaptyUI will automatically fetch the necessary products. |
| **ViewConfiguration** | required | An `AdaptyUI.LocalizedViewConfiguration` object containing visual details of the paywall. Use the `AdaptyUI.getViewConfiguration(paywall:locale:)` method. Refer to [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls) topic for more details. |
| **Delegate** | required | An `AdaptyPaywallControllerDelegate` to listen to paywall events. Refer to [Handling paywall events](ios-handling-events) topic for more details. |
| **TagResolver** | optional | Define a dictionary of custom tags and their resolved values. Custom tags serve as placeholders in the paywall content, dynamically replaced with specific strings for personalized content within the paywall. Refer to [Custom tags in Paywall Builder](custom-tags-in-paywall-builder) topic for more details. |
Returns:
| Object | Description |
| :---------------------- | :--------------------------------------------------- |
| AdaptyPaywallController | An object, representing the requested paywall screen |
After the object has been successfully created, you can display it on the screen of the device:
```swift showLineNumbers title="Swift"
present(visualPaywall, animated: true)
```
## Present paywalls in SwiftUI
In order to display the visual paywall on the device screen, use the `.paywall` modifier in SwiftUI:
```swift showLineNumbers title="SwiftUI"
@State var paywallPresented = false
var body: some View {
Text("Hello, AdaptyUI!")
.paywall(
isPresented: $paywallPresented,
paywall: ,
configuration: ,
didPerformAction: { action in
switch action {
case .close:
paywallPresented = false
default:
// Handle other actions
break
}
},
didFinishPurchase: { product, profile in paywallPresented = false },
didFailPurchase: { product, error in /* handle the error */ },
didFinishRestore: { profile in /* check access level and dismiss */ },
didFailRestore: { error in /* handle the error */ },
didFailRendering: { error in paywallPresented = false }
)
}
```
Request parameters:
| Parameter | Presence | Description |
| :---------------- | :------- | :----------------------------------------------------------- |
| **Paywall** | required | An `AdaptyPaywall` object to obtain a controller for the desired paywall. |
| **Product** | optional | Provide an array of `AdaptyPaywallProducts` to optimize the display timing of products on the screen. If `nil` is passed, AdaptyUI will automatically fetch the necessary products. |
| **Configuration** | required | An `AdaptyUI.LocalizedViewConfiguration` object containing visual details of the paywall. Use the `AdaptyUI.getViewConfiguration(paywall:locale:)` method. Refer to [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls) topic for more details. |
| **TagResolver** | optional | Define a dictionary of custom tags and their resolved values. Custom tags serve as placeholders in the paywall content, dynamically replaced with specific strings for personalized content within the paywall. Refer to [Custom tags in Paywall Builder](custom-tags-in-paywall-builder) topic for more details. |
Closure parameters:
| Closure parameter | Description |
| :-------------------- | :-------------------------------------------------------------------------------- |
| **didFinishPurchase** | If Adapty.makePurchase() succeeds, this callback will be invoked. |
| **didFailPurchase** | If Adapty.makePurchase() fails, this callback will be invoked. |
| **didFinishRestore** | If Adapty.restorePurchases() succeeds, this callback will be invoked. |
| **didFailRestore** | If Adapty.restorePurchases() fails, this callback will be invoked. |
| **didFailRendering** | If an error occurs during the interface rendering, this callback will be invoked. |
Refer to the [iOS - Handling events](ios-handling-events) topic for other closure parameters.
**Next step:**
- [Handle paywall events](ios-handling-events-legacy)
---
# File: ios-handling-events-legacy
---
---
title: "Handle paywall events in legacy iOS SDK"
description: "Handle events in iOS (Legacy) apps with Adapty’s event tracking system."
---
Paywalls configured with the [Paywall Builder](adapty-paywall-builder-legacy) don't need extra code to make and restore purchases. However, they generate some events that your app can respond to. Those events include button presses (close buttons, URLs, product selections, and so on) as well as notifications on purchase-related actions taken on the paywall. Learn how to respond to these events below.
:::warning
This guide is for **legacy Paywall Builder paywalls** only which require Adapty SDK up to v2.x. For presenting paywalls in Adapty SDK v3.0 or later designed with new Paywall Builder, see [iOS - Handle paywall events designed with new Paywall Builder](ios-handling-events).
:::
## Handling events in Swift
To control or monitor processes occurring on the paywall screen within your mobile app, implement the `AdaptyPaywallControllerDelegate` methods.
### User-generated events
#### Actions
If a user performs some action (`close`, `openURL(url:)` or `custom(id:)`), the method below will be invoked. Note that this is just an example and you can implement the response to actions differently:
```swift showLineNumbers title="Swift"
func paywallController(_ controller: AdaptyPaywallController,
didPerform action: AdaptyUI.Action) {
switch action {
case .close:
controller.dismiss(animated: true)
case let .openURL(url):
// handle URL opens (incl. terms and privacy links)
UIApplication.shared.open(url, options: [:])
case let .custom(id):
if id == "login" {
// implement login flow
}
break
}
}
```
For example, if a user taps the close button, the action `close` will occur and you are supposed to dismiss the paywall. Note that at the very least you need to implement the reactions to both `close` and `openURL`.
> 💡 Login Action
>
> If you have configured Login Action in the dashboard, you should also implement reaction for custom action with id `"login"`.
#### Product selection
If a user selects a product for purchase, this method will be invoked:
```swift showLineNumbers title="Swift"
func paywallController(_ controller: AdaptyPaywallController,
didSelectProduct product: AdaptyPaywallProduct) {
}
```
#### Started purchase
If a user initiates the purchase process, this method will be invoked:
```swift showLineNumbers title="Swift"
func paywallController(_ controller: AdaptyPaywallController,
didStartPurchase product: AdaptyPaywallProduct) {
}
```
It will not be invoked in Observer mode. Refer to the [iOS - Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode) topic for details.
#### Canceled purchase
If a user initiates the purchase process but manually interrupts it, the method below will be invoked. This event occurs when the `Adapty.makePurchase()` function completes with a `.paymentCancelled` error:
```swift showLineNumbers title="Swift"
func paywallController(_ controller: AdaptyPaywallController,
didCancelPurchase product: AdaptyPaywallProduct) {
}
```
It will not be invoked in Observer mode. Refer to the [iOS - Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode) topic for details.
#### Successful purchase
If `Adapty.makePurchase()` succeeds, this method will be invoked:
```swift showLineNumbers title="Swift"
func paywallController(_ controller: AdaptyPaywallController,
didFinishPurchase product: AdaptyPaywallProduct,
purchasedInfo: AdaptyPurchasedInfo) {
controller.dismiss(animated: true)
}
```
We recommend dismissing the paywall screen in that case.
It will not be invoked in Observer mode. Refer to the [iOS - Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode) topic for details.
#### Failed purchase
If `Adapty.makePurchase()` fails, this method will be invoked:
```swift showLineNumbers title="Swift"
func paywallController(_ controller: AdaptyPaywallController,
didFailPurchase product: AdaptyPaywallProduct,
error: AdaptyError) {
}
```
It will not be invoked in Observer mode. Refer to the [iOS - Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode) topic for details.
#### Successful restore
If `Adapty.restorePurchases()` succeeds, this method will be invoked:
```swift showLineNumbers title="Swift"
func paywallController(_ controller: AdaptyPaywallController,
didFinishRestoreWith profile: AdaptyProfile) {
}
```
We recommend dismissing the screen if a the has the required `accessLevel`. Refer to the [Subscription status](subscription-status) topic to learn how to check it.
#### Failed restore
If `Adapty.restorePurchases()` fails, this method will be invoked:
```swift showLineNumbers title="Swift"
public func paywallController(_ controller: AdaptyPaywallController,
didFailRestoreWith error: AdaptyError) {
}
```
### Data fetching and rendering
#### Product loading errors
If you don't pass the product array during the initialization, AdaptyUI will retrieve the necessary objects from the server by itself. If this operation fails, AdaptyUI will report the error by calling this method:
```swift showLineNumbers title="Swift"
public func paywallController(_ controller: AdaptyPaywallController,
didFailLoadingProductsWith error: AdaptyError) -> Bool {
return true
}
```
If you return `true`, AdaptyUI will repeat the request after 2 seconds.
#### Rendering errors
If an error occurs during the interface rendering, it will be reported by this method:
```swift showLineNumbers title="Swift"
public func paywallController(_ controller: AdaptyPaywallController,
didFailRenderingWith error: AdaptyError) {
}
```
In a normal situation, such errors should not occur, so if you come across one, please let us know.
## Handling events in SwiftUI
To control or monitor processes occurring on the paywall screen within your mobile app, use the `.paywall` modifier in SwiftUI:
```swift showLineNumbers title="Swift"
@State var paywallPresented = false
var body: some View {
Text("Hello, AdaptyUI!")
.paywall(
isPresented: $paywallPresented,
paywall: paywall,
configuration: viewConfig,
didPerformAction: { action in
switch action {
case .close:
paywallPresented = false
case .openURL(url):
// handle opening the URL (incl. for terms and privacy)
case
default:
// handle other actions
break
}
},
didSelectProduct: { /* Handle the event */ },
didStartPurchase: { /* Handle the event */ },
didFinishPurchase: { product, info in /* Handle the event */ },
didFailPurchase: { product, error in /* Handle the event */ },
didCancelPurchase: { /* Handle the event */ },
didStartRestore: { /* Handle the event */ },
didFinishRestore: { /* Handle the event */ },
didFailRestore: { /* Handle the event */ },
didFailRendering: { error in
paywallPresented = false
},
didFailLoadingProducts: { error in
return false
}
)
}
```
You can register only the closure parameters you need, and omit those you do not need. In this case, unused closure parameters will not be created.
| Closure parameter | Description |
| :------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **didSelectProduct** | If a user selects a product for purchase, this parameter will be invoked. |
| **didStartPurchase** | If a user initiates the purchase process, this parameter will be invoked. |
| **didFinishPurchase** | If Adapty.makePurchase() succeeds, this parameter will be invoked. |
| **didFailPurchase** | If Adapty.makePurchase() fails, this parameter will be invoked. |
| **didCancelPurchase** | If a user initiates the purchase process but manually interrupts it, this parameter will be invoked. |
| **didStartRestore** | If a user initiates the purchase restoration, this parameter will be invoked. |
| **didFinishRestore** | If `Adapty.restorePurchases()` succeeds, this parameter will be invoked. |
| **didFailRestore** | If `Adapty.restorePurchases()` fails, this parameter will be invoked. |
| **didFailRendering** | If an error occurs during the interface rendering, this parameter will be invoked. |
| **didFailLoadingProducts** | If you don't pass the product array during the initialization, AdaptyUI will retrieve the necessary objects from the server by itself. If this operation fails, AdaptyUI will invoke this parameter. |
Note that at the very least you need to implement the reactions to both `close` and `openURL`.
---
# End of Documentation
_Generated on: 2026-03-11T12:58:11.730Z_
_Successfully processed: 43/43 files_