---
title: "Enable purchases with Flow Builder in iOS SDK"
description: "Quickstart guide to enabling in-app purchases with Adapty Flow Builder."
---

To enable in-app purchases, you need to understand three key concepts:

- [**Products**](product) – anything users can buy (subscriptions, consumables, lifetime access)
- [**Flows**](adapty-flow-builder) – screen sequences that present products to users, built in the no-code Flow Builder. The SDK retrieves them via `getFlow`. If you'd rather build the UI in your own code, use a paywall instead — see [Implement paywalls manually](ios-quickstart-manual).
- [**Placements**](placements) – where and when you show flows in your app (like `main`, `onboarding`, `settings`). You attach flows 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 flows 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 Flow Builder | ✅ Easy | You [create a complete, purchase-ready flow 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 flow 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 flow created in the Adapty Flow Builder.**

If you'd rather build the paywall UI yourself, see [Implement paywalls manually](ios-quickstart-manual).
:::

To display a flow created in the Adapty Flow Builder, in your app code, you only need to:

1. **Get the flow**: Get it from Adapty.
2. **Display it and Adapty will handle purchases for you**: Show the view in your app.
3. **Handle button actions**: Associate user interactions with your app's response to them. For example, open links or close the flow 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 flow and add products to it](create-paywall).
4. [Create a placement and add your flow to it](create-placement).
5. [Install and activate the Adapty SDK](sdk-installation-ios) in your app code. This guide uses Adapty iOS SDK v4 APIs.

## 1. Get the flow

Your flows are associated with placements configured in the dashboard. Placements allow you to run different flows for different audiences or to run [A/B tests](ab-tests).

To get a flow created in the Adapty Flow Builder, you need to:

1. Get the `flow` object by the [placement](placements) ID using the `getFlow` method and check whether it has a view configuration.
2. Get the view configuration using the `getFlowConfiguration` method. It contains the UI elements and styling needed to display the flow.

```swift

func loadFlow() async {
    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)
}
```

## 2. Display the flow

Now, when you have the flow configuration, it's enough to add a few lines to display your flow.

<Tabs groupId="current-os" queryString>

<TabItem value="swiftui" label="SwiftUI" default>

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.
:::

```swift
.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)")
    }
)
```
</TabItem>

<TabItem value="uikit" label="UIKit" default>

```swift

func presentFlow(with config: AdaptyUI.FlowConfiguration) {
    let flowController = try AdaptyUI.flowController(
        with: config,
        delegate: self
    )
    present(flowController, animated: true)
}
```

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:

```swift
extension YourViewController: AdaptyFlowControllerDelegate {
    func flowController(_ controller: AdaptyFlowController,
                        didFinishPurchase product: AdaptyPaywallProduct,
                        purchaseResult: AdaptyPurchaseResult) {
        if !purchaseResult.isPurchaseCancelled {
            controller.dismiss(animated: true) // or do nothing to let the flow continue
        }
    }

    func flowController(_ controller: AdaptyFlowController,
                        didFailPurchase product: AdaptyPaywallProduct,
                        error: AdaptyError) {
        print("Purchase failed: \(error)")
    }

    func flowController(_ controller: AdaptyFlowController,
                        didFinishRestoreWith profile: AdaptyProfile) {
        print("Restore finished successfully")
    }

    func flowController(_ controller: AdaptyFlowController,
                        didFailRestoreWith error: AdaptyError) {
        print("Restore failed: \(error)")
    }
}
```
</TabItem>
</Tabs>

:::info
For more details on how to display a flow, see our [guide](ios-present-paywalls).
:::

## 3. Handle button actions

When users click buttons, the iOS SDK automatically handles purchases, restoration, closing the flow, 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](handle-paywall-actions) and [events](ios-handling-events).
:::

<Tabs groupId="current-os" queryString>

<TabItem value="swiftui" label="SwiftUI" default>

```swift
.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 }
)
```
</TabItem>

<TabItem value="uikit" label="UIKit" default>

```swift
extension YourViewController: AdaptyFlowControllerDelegate {
    func flowController(_ controller: AdaptyFlowController,
                        didPerform action: AdaptyUI.Action) {
        switch action {
            case .close:
                controller.dismiss(animated: true) // default behavior — override only if needed
            default:
                break
        }
    }
}
```
</TabItem>
</Tabs>

## Next steps

:::tip
Have questions or running into issues? Check out our [support forum](https://adapty.featurebase.app/) where you can find answers to common questions or ask your own. Our team and community are here to help!
:::

Your flow 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.

Now, you need to [check the users' access level](ios-check-subscription-status) to ensure you display a flow 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.

<Tabs groupId="current-os" queryString>

<TabItem value="swiftui" label="SwiftUI" default>

```swift

struct 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)")
        }
    }
}
```
</TabItem>

<TabItem value="uikit" label="UIKit" default>

```swift

class ViewController: UIViewController {
    private var flowConfiguration: AdaptyUI.FlowConfiguration?

    override func viewDidLoad() {
        super.viewDidLoad()

        Task {
            await initializeFlow()
        }
    }

    private func initializeFlow() async {
        do {
            flowConfiguration = try await loadFlow()

            if let flowConfiguration {
                await MainActor.run {
                    presentFlow(with: flowConfiguration)
                }
            }
        } catch {
            print("Error initializing: \(error)")
        }
    }

    private func loadFlow() async throws -> AdaptyUI.FlowConfiguration? {
        let flow = try await Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID")

        guard flow.hasViewConfiguration else {
            print("Flow doesn't have a view configuration")
            return nil
        }

        return try await AdaptyUI.getFlowConfiguration(forFlow: flow)
    }

    private func presentFlow(with config: AdaptyUI.FlowConfiguration) {
        guard let flowController = try? AdaptyUI.flowController(
            with: config,
            delegate: self
        ) else { return }

        present(flowController, animated: true)
    }
}

extension ViewController: AdaptyFlowControllerDelegate {
    func flowController(_ controller: AdaptyFlowController,
                        didFinishPurchase product: AdaptyPaywallProduct,
                        purchaseResult: AdaptyPurchaseResult) {
        if !purchaseResult.isPurchaseCancelled {
            controller.dismiss(animated: true) // or do nothing to let the flow continue
        }
    }

    func flowController(_ controller: AdaptyFlowController,
                        didFailPurchase product: AdaptyPaywallProduct,
                        error: AdaptyError) {
        print("Purchase failed for \(product.vendorProductId): \(error)")

        guard error.adaptyErrorCode != .paymentCancelled else { return }

        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)
        alert.addAction(UIAlertAction(title: "OK", style: .default))
        present(alert, animated: true)
    }

    func flowController(_ controller: AdaptyFlowController,
                        didFinishRestoreWith profile: AdaptyProfile) {
        print("Restore finished successfully")
        controller.dismiss(animated: true)
    }

    func flowController(_ controller: AdaptyFlowController,
                        didFailRestoreWith error: AdaptyError) {
        print("Restore failed: \(error)")
    }

    func flowController(_ controller: AdaptyFlowController,
                        didReceiveError error: AdaptyUIError) {
        print("Flow error: \(error)")
        controller.dismiss(animated: true)
    }
}
```
</TabItem>
</Tabs>