---
title: "Habilitar compras con Flow Builder en iOS SDK"
description: "Guía de inicio rápido para habilitar compras in-app con Adapty Flow Builder."
---

Para habilitar las compras in-app, necesitas entender tres conceptos clave:
- [**Productos**](product) – todo lo que los usuarios pueden comprar (suscripciones, consumibles, acceso de por vida)
- [**Flows**](adapty-flow-builder) – secuencias de pantallas que presentan productos a los usuarios, creadas en el Flow Builder sin código. El SDK los recupera mediante `getFlow`. Si prefieres construir la interfaz en tu propio código, usa un paywall en su lugar — consulta [Implementar paywalls manualmente](ios-quickstart-manual).
- [**Placements**](placements) – dónde y cuándo muestras los flows en tu app (por ejemplo, `main`, `onboarding`, `settings`). Vincula los flows a los placements en el dashboard y luego solicítalos por ID de placement en tu código. Esto facilita ejecutar pruebas A/B y mostrar flows distintos a diferentes usuarios.
Adapty te ofrece tres formas de habilitar compras en tu aplicación. Selecciona la que mejor se adapte a tus requisitos:
| Implementación | Complejidad | Cuándo usarlo |
|---|---|---|
| Adapty Flow Builder | ✅ Fácil | [Creas un flow completo y listo para compras en el editor sin código](quickstart-paywalls). Adapty lo renderiza automáticamente y gestiona todo el proceso de compra, la validación de recibos y la gestión de suscripciones. |
| Paywalls creados manualmente | 🟡 Media | Implementas la UI de tu paywall en el código de tu app, pero sigues obteniendo el objeto flow desde Adapty para mantener flexibilidad en los productos ofrecidos. Consulta la [guía](ios-quickstart-manual). |
| Modo Observer | 🔴 Difícil | Ya tienes tu propia infraestructura de gestión de compras y quieres seguir usándola. Ten en cuenta que el modo observer tiene sus limitaciones en Adapty. Consulta el [artículo](observer-vs-full-mode). |
:::important
**Los pasos a continuación muestran cómo implementar un flow creado en el Adapty Flow Builder.**

Si prefieres construir la UI del paywall tú mismo, consulta [Implementar paywalls manualmente](ios-quickstart-manual).
:::

Para mostrar un flow creado en el Adapty Flow Builder, en el código de tu app solo necesitas:

1. **Obtener el flow**: Obtenlo desde Adapty.
2. **Mostrarlo y Adapty gestionará las compras por ti**: Muestra la vista en tu app.
3. **Gestionar las acciones de los botones**: Asocia las interacciones del usuario con la respuesta de tu app. Por ejemplo, abrir enlaces o cerrar el flow cuando los usuarios pulsen botones.
## Antes de empezar \{#before-you-start\}

Antes de empezar, completa estos pasos:

1. [Conecta tu app con la App Store](initial_ios) en el Adapty Dashboard.
2. [Crea tus productos](create-product) en Adapty.
3. [Crea un flow y añade productos a él](create-paywall).
4. [Crea un placement y añade tu flow a él](create-placement).
5. [Instala y activa el SDK de Adapty](sdk-installation-ios) en el código de tu app. Esta guía usa las APIs del SDK de Adapty iOS v4.

## 1. Obtener el flow \{#1-get-the-flow\}

Tus flows están asociados a placements configurados en el dashboard. Los placements te permiten ejecutar diferentes flows para distintas audiencias o ejecutar [pruebas A/B](ab-tests).

Para obtener un flow creado en el Adapty Flow Builder, necesitas:

1. Obtener el objeto `flow` por el ID del [placement](placements) usando el método `getFlow` y comprobar si tiene una configuración de vista.
2. Obtener la configuración de vista usando el método `getFlowConfiguration`. Contiene los elementos de UI y el estilo necesarios para mostrar el 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. Mostrar el flow \{#2-display-the-flow\}

Ahora que tienes la configuración del flow, basta con añadir unas pocas líneas para mostrarlo.

<Tabs groupId="current-os" queryString>

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

En SwiftUI, al mostrar el flow, también necesitas gestionar los eventos. `didFinishPurchase`, `didFailPurchase`, `didFinishRestore`, `didFailRestore` y `didReceiveError` son obligatorios. Durante las pruebas, puedes copiar el código del fragmento de abajo para registrar estos eventos.
:::tip
El flow no se cierra automáticamente tras una compra exitosa. En `didFinishPurchase`, establece tu binding de presentación en `false` para cerrarlo, o no hagas nada para que el flow continúe a sus siguientes pantallas.
:::
```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)
}
```
Implementa `AdaptyFlowControllerDelegate` para gestionar eventos. Como mínimo, implementa los cuatro métodos que no tienen implementación por defecto. Ten en cuenta que el controlador no se cierra automáticamente tras una compra exitosa — ciérralo en `didFinishPurchase`, o no hagas nada para dejar que el flow continúe a sus siguientes pantallas:
```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
Para más detalles sobre cómo mostrar un flow, consulta nuestra [guía](ios-present-paywalls).
:::
## 3. Gestionar las acciones de los botones \{#handle-button-actions\}

Cuando los usuarios pulsan botones, el SDK de iOS gestiona automáticamente las compras, la restauración, el cierre del flow y la apertura de enlaces.

Sin embargo, otros botones tienen IDs personalizados o predefinidos y requieren que gestiones las acciones en tu código. O puede que quieras sobreescribir su comportamiento por defecto.

Por ejemplo, así es como gestionar el botón de cierre. En UIKit, el SDK descarta el controlador automáticamente cuando se dispara `.close` — sobreescríbelo solo si quieres un comportamiento personalizado. En SwiftUI, debes establecer tu binding `isPresented` a `false` tú mismo.
:::tip
Lee nuestras guías sobre cómo gestionar [acciones](handle-paywall-actions) y [eventos](ios-handling-events) de los botones.
:::

<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>
## Próximos pasos \{#next-steps\}

:::tip
¿Tienes preguntas o estás teniendo algún problema? Consulta nuestro [foro de soporte](https://adapty.featurebase.app/) donde encontrarás respuestas a preguntas frecuentes o podrás plantear las tuyas. ¡Nuestro equipo y la comunidad están aquí para ayudarte!
:::

Tu paywall está listo para mostrarse en la app. [Prueba tus compras en modo sandbox](test-purchases-in-sandbox) para asegurarte de que puedes completar una compra de prueba desde el paywall.

Ahora necesitas [comprobar el nivel de acceso de los usuarios](ios-check-subscription-status) para asegurarte de mostrar un paywall o dar acceso a las funciones de pago a los usuarios correctos.

## Ejemplo completo \{#full-example\}

Aquí puedes ver cómo integrar todos los pasos de esta guía en tu app.

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