---
title: "Kích hoạt mua hàng với Flow Builder trong iOS SDK"
description: "Hướng dẫn nhanh để kích hoạt in-app purchase với Adapty Flow Builder."
---

Để kích hoạt in-app purchase, bạn cần hiểu ba khái niệm chính:
- [**Sản phẩm**](product) – mọi thứ người dùng có thể mua (gói đăng ký, consumable, quyền truy cập trọn đời)
- [**Flows**](adapty-flow-builder) – chuỗi màn hình giới thiệu sản phẩm đến người dùng, được xây dựng trong Flow Builder không cần code. SDK truy xuất chúng qua `getFlow`. Nếu bạn muốn tự xây dựng giao diện bằng code, hãy dùng paywall — xem [Triển khai paywall thủ công](ios-quickstart-manual).
- [**Placements**](placements) – nơi và thời điểm hiển thị flow trong ứng dụng (ví dụ: `main`, `onboarding`, `settings`). Bạn gắn flow vào placement trên dashboard, sau đó yêu cầu chúng theo placement ID trong code. Cách này giúp bạn dễ dàng chạy A/B test và hiển thị các flow khác nhau cho từng nhóm người dùng.
Adapty cung cấp cho bạn ba cách để kích hoạt mua hàng trong ứng dụng. Hãy chọn một trong số đó tùy theo yêu cầu của ứng dụng:
| Cách triển khai | Độ phức tạp | Khi nào nên dùng |
|---|---|---|
| Adapty Flow Builder | ✅ Dễ | Bạn [tạo một flow hoàn chỉnh, sẵn sàng mua hàng trong trình tạo no-code](quickstart-paywalls). Adapty tự động hiển thị và xử lý toàn bộ flow mua hàng, xác thực biên lai, và quản lý gói đăng ký. |
| Paywall tự tạo | 🟡 Trung bình | Bạn tự triển khai giao diện paywall trong code của mình, nhưng vẫn lấy đối tượng flow từ Adapty để duy trì sự linh hoạt trong việc cung cấp sản phẩm. Xem [hướng dẫn](ios-quickstart-manual). |
| Chế độ observer | 🔴 Khó | Bạn đã có hạ tầng xử lý mua hàng riêng và muốn tiếp tục dùng nó. Lưu ý rằng chế độ observer có một số hạn chế trong Adapty. Xem [bài viết](observer-vs-full-mode). |
:::important
**Các bước dưới đây hướng dẫn cách triển khai một flow được tạo trong Adapty Flow Builder.**

Nếu bạn muốn tự xây dựng giao diện paywall, hãy xem [Triển khai paywall thủ công](ios-quickstart-manual).
:::

Để hiển thị một flow được tạo trong Adapty Flow Builder, trong code ứng dụng, bạn chỉ cần:

1. **Lấy flow**: Lấy flow từ Adapty.
2. **Hiển thị flow và Adapty sẽ xử lý thanh toán cho bạn**: Hiển thị view trong ứng dụng.
3. **Xử lý các hành động của nút**: Liên kết các thao tác của người dùng với phản hồi tương ứng trong ứng dụng. Ví dụ: mở liên kết hoặc đóng flow khi người dùng nhấn nút.
## Trước khi bắt đầu \{#before-you-start\}

Trước khi bắt đầu, hãy hoàn thành các bước sau:

1. [Kết nối ứng dụng với App Store](initial_ios) trong Adapty Dashboard.
2. [Tạo sản phẩm](create-product) trong Adapty.
3. [Tạo flow và thêm sản phẩm vào đó](create-paywall).
4. [Tạo placement và thêm flow vào đó](create-placement).
5. [Cài đặt và kích hoạt SDK](sdk-installation-ios) trong code của ứng dụng. Hướng dẫn này sử dụng Adapty iOS SDK v4 APIs.

## 1. Lấy flow \{#get-the-flow\}

Các flow của bạn được liên kết với các placement được cấu hình trong dashboard. Placement cho phép bạn chạy các flow khác nhau cho các đối tượng khác nhau hoặc chạy [A/B test](ab-tests).

Để lấy một flow đã tạo trong Adapty Flow Builder, bạn cần:

1. Lấy đối tượng `flow` theo ID [placement](placements) bằng phương thức `getFlow` và kiểm tra xem nó có cấu hình view hay không.
2. Lấy cấu hình view bằng phương thức `getFlowConfiguration`. Cấu hình này chứa các thành phần UI và styling cần thiết để hiển thị 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. Hiển thị flow \{#2-display-the-flow\}

Bây giờ, khi bạn đã có cấu hình flow, chỉ cần thêm vài dòng code để hiển thị flow của mình.

<Tabs groupId="current-os" queryString>

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

Trong SwiftUI, khi hiển thị flow, bạn cũng cần xử lý các sự kiện. `didFinishPurchase`, `didFailPurchase`, `didFinishRestore`, `didFailRestore` và `didReceiveError` là bắt buộc. Khi kiểm thử, bạn có thể sao chép đoạn code bên dưới để ghi log các sự kiện này.
:::tip
Flow không tự động đóng sau khi mua hàng thành công. Trong `didFinishPurchase`, hãy đặt binding hiển thị của bạn thành `false` để đóng flow, hoặc không làm gì để flow tiếp tục chuyển sang các màn hình tiếp theo.
:::
```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` để xử lý các sự kiện. Tối thiểu, hãy implement bốn phương thức không có cài đặt mặc định. Lưu ý rằng controller sẽ không tự dismiss sau khi mua thành công — hãy dismiss nó trong `didFinishPurchase`, hoặc không làm gì để flow tiếp tục sang các màn hình tiếp theo:
```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
Để biết thêm chi tiết về cách hiển thị một flow, hãy xem [hướng dẫn](ios-present-paywalls) của chúng tôi.
:::
## 3. Xử lý hành động nút \{#handle-button-actions\}

Khi người dùng nhấn các nút, iOS SDK tự động xử lý việc mua hàng, khôi phục, đóng flow và mở liên kết.

Tuy nhiên, một số nút có ID tùy chỉnh hoặc được định sẵn và cần xử lý hành động trong code của bạn. Hoặc, bạn có thể muốn ghi đè hành vi mặc định của chúng.

Ví dụ, dưới đây là cách xử lý nút đóng. Trong UIKit, SDK tự động dismiss controller khi `.close` được kích hoạt — chỉ ghi đè nếu bạn muốn hành vi tùy chỉnh. Trong SwiftUI, bạn phải tự đặt binding `isPresented` của mình thành `false`.
:::tip
Đọc hướng dẫn của chúng tôi về cách xử lý [hành động](handle-paywall-actions) và [sự kiện](ios-handling-events) của nút.
:::

<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>
## Các bước tiếp theo \{#next-steps\}

:::tip
Bạn có câu hỏi hoặc gặp sự cố? Hãy xem [diễn đàn hỗ trợ](https://adapty.featurebase.app/) của chúng tôi — nơi bạn có thể tìm câu trả lời cho các câu hỏi thường gặp hoặc đặt câu hỏi của riêng mình. Đội ngũ và cộng đồng của chúng tôi luôn sẵn sàng giúp đỡ!
:::

Flow của bạn đã sẵn sàng để hiển thị trong app. [Kiểm thử mua hàng trong chế độ sandbox](test-purchases-in-sandbox) để đảm bảo bạn có thể hoàn tất một giao dịch mua thử.

Tiếp theo, bạn cần [kiểm tra mức độ truy cập của người dùng](ios-check-subscription-status) để đảm bảo bạn hiển thị flow hoặc cấp quyền truy cập vào các tính năng trả phí cho đúng người dùng.

## Ví dụ đầy đủ \{#full-example\}

Dưới đây là cách tích hợp tất cả các bước trong hướng dẫn này vào ứng dụng của bạn.

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