Skills cover SDK integration, building flows and paywalls, pre-publish audits, placement migration, and more. Describe the task in plain language and your agent picks the skill.
To enable in-app purchases, you need to understand three key concepts:
Products – anything users can buy (subscriptions, consumables, lifetime access)
Flows & paywalls – screens that present products to users, built in the no-code Flow & Paywall Builder. The SDK retrieves them via getFlow. If you’d rather build the UI in your own code, see Implement custom paywalls.
Placements – where and when you show flows & paywalls in your app (like main, onboarding, settings). You attach them to 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 ones to different users.
Adapty offers you three ways to enable purchases in your app. Select one of them depending on your app requirements:
You implement your paywall UI in your app code, but still get the flow object from Adapty to maintain flexibility in product offerings. See the guide.
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.
Important
The steps below show how to implement a flow or paywall created in the Adapty Flow & Paywall Builder.
To display a flow or paywall created in the Adapty Flow & Paywall Builder, in your app code, you only need to:
Get the flow or paywall: Get it from Adapty.
Display it and Adapty will handle purchases for you: Show the view in your app.
Handle button actions: Associate user interactions with your app’s response to them. For example, open links or close the paywall when users click buttons.
Your flows and paywalls are associated with placements configured in the dashboard. Placements allow you to run different flows or paywalls for different audiences or to run A/B tests.
To get a flow or paywall created in the Adapty Flow & Paywall Builder, you need to:
Get the flow object by the placement ID using the getFlow method and check whether it has a view configuration.
Get the view configuration using the getFlowConfiguration method. It contains the UI elements and styling needed to display it.
Now, when you have the flow configuration, it’s enough to add a few lines to display your flow or paywall.
In SwiftUI, when displaying the flow, you also need to handle events. didFinishPurchase, didFailPurchase, didFinishRestore, didFailRestore, and didReceiveError are required. When testing, you can just copy the code from the snippet below to log these events.
Tip
The flow does not dismiss automatically after a successful purchase. In didFinishPurchase, set your presentation binding to false to dismiss it, or do nothing to let the flow continue to its next screens.
.flow( isPresented: $flowPresented, flowConfiguration: flowConfiguration, didFinishPurchase: { product, purchaseResult in print("Purchase finished successfully") flowPresented = false // or do nothing to let the flow continue }, didFailPurchase: { product, error in print("Purchase failed: \(error)") }, didFinishRestore: { profile in print("Restore finished successfully") }, didFailRestore: { error in print("Restore failed: \(error)") }, didReceiveError: { error in flowPresented = false print("Flow error: \(error)") })
Implement AdaptyFlowControllerDelegate to handle events. At minimum, implement the four methods that have no default implementation. Note that the controller does not dismiss itself after a successful purchase — dismiss it in didFinishPurchase, or do nothing to let the flow continue to its next screens:
For more details on how to display a flow or paywall, see our guide.
3. Handle button actions
When users click buttons, the iOS SDK automatically handles purchases, restoration, closing the screen, 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 how to handle the close button. In UIKit, the SDK dismisses the controller automatically when .close fires — override only if you want custom behavior. In SwiftUI, you must set your isPresented binding to false yourself.
Tip
Read our guides on how to handle button actions and events.
.flow( isPresented: $flowPresented, flowConfiguration: flowConfiguration, didPerformAction: { action in switch action { case .close: flowPresented = false // dismiss the flow when the user taps close default: break } }, didFinishPurchase: { product, purchaseResult in flowPresented = false }, didFailPurchase: { product, error in /* handle the error */ }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, didReceiveError: { error in flowPresented = false })
Have questions or running into issues? Check out our support forum where you can find answers to common questions or ask your own. Our team and community are here to help!
Your flow or paywall is ready to be displayed in the app. Test your purchases in sandbox mode to make sure you can complete a test purchase.
Now, you need to check the users’ access level to ensure you display a paywall or give access to paid features to the right users.
Full example
Here is how all the steps from this guide can be integrated in your app together.
import SwiftUIimport Adaptyimport AdaptyUIstruct ContentView: View { @State private var flowPresented = false @State private var flowConfiguration: AdaptyUI.FlowConfiguration? @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 initializeFlow() hasInitialized = true } .flow( isPresented: $flowPresented, flowConfiguration: flowConfiguration, didPerformAction: { action in switch action { case .close: flowPresented = false default: break } }, didFinishPurchase: { product, purchaseResult in print("Purchase finished successfully") flowPresented = false // or do nothing to let the flow continue }, didFailPurchase: { product, error in print("Purchase failed: \(error)") }, didFinishRestore: { profile in print("Restore finished successfully") }, didFailRestore: { error in print("Restore failed: \(error)") }, didReceiveError: { error in print("Flow error: \(error)") flowPresented = false } ) } private func initializeFlow() async { isLoading = true defer { isLoading = false } await loadFlow() flowPresented = true } private func loadFlow() async { do { let flow = try await Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID") guard flow.hasViewConfiguration else { print("Flow doesn't have a view configuration") return } flowConfiguration = try await AdaptyUI.getFlowConfiguration(forFlow: flow) } catch { print("Failed to load: \(error)") } }}