---
title: "Migrate Adapty iOS SDK sang v4.0"
description: "Migrate sang Adapty iOS SDK v4.0 (beta) bằng cách thay thế các API paywall bằng API flow, tương thích với cả Flow Builder và Paywall Builder."
---

Adapty iOS SDK 4.0 (beta) giới thiệu flows và đổi tên các API paywall tương ứng. Các API mới hoạt động với cả Flow Builder mới và Paywall Builder hiện có — không cần thay đổi cấu hình nào trên Adapty Dashboard.
## Tham chiếu nhanh \{#quick-reference\}
| v3 | v4 |
|---|---|
| `Adapty.getPaywall(placementId:locale:)` | `Adapty.getFlow(placementId:)` |
| `AdaptyUI.getPaywallConfiguration(forPaywall:)` | `AdaptyUI.getFlowConfiguration(forFlow:locale:)` |
| `Adapty.getPaywallProducts(paywall:)` | `Adapty.getPaywallProducts(flow:)` |
| `Adapty.logShowPaywall(_:)` | `Adapty.logShowFlow(_:)` |
| `AdaptyPaywallController` | `AdaptyFlowController` |
| `AdaptyPaywallControllerDelegate` | `AdaptyFlowControllerDelegate` |
| `AdaptyUI.paywallController(with:delegate:)` | `AdaptyUI.flowController(with:delegate:)` |
| `.paywall()` (SwiftUI modifier) | `.flow()` |
| `AdaptyPaywallView` | `AdaptyFlowView` |
| `didFailRenderingWith:` / `didFailRendering:` | `didReceiveError:` |
| `didFinishPurchase` (tùy chọn, tự động đóng khi thành công) | `didFinishPurchase` (bắt buộc, không tự động đóng) |
| Sản phẩm package `Adapty_KidsMode` / `AdaptyUI_KidsMode` | Package trait `KidsMode` |
| `Adapty.updateAttribution(_:source:)` (`source: String`) | `Adapty.updateAttribution(_:source:)` (`source: AdaptyAttributionSource`) |
| `Adapty.setIntegrationIdentifier(key:value:)` | `Adapty.setIntegrationIdentifier(_:)` (`AdaptyIntegrationIdentifier`) |
## Phiên bản iOS tối thiểu \{#minimum-ios-version\}

Adapty iOS SDK 4.0 nâng yêu cầu deployment target tối thiểu từ iOS 13.0 lên **iOS 15.0**. Hãy đặt iOS Deployment Target của dự án lên 15.0 hoặc cao hơn trước khi nâng cấp.
## Cài đặt: CocoaPods không còn được hỗ trợ \{#installation-cocoapods-no-longer-supported\}

Adapty iOS SDK 4.0 không còn hỗ trợ CocoaPods. Hãy cài đặt SDK bằng [Swift Package Manager](sdk-installation-ios#install-adapty-sdk).

Nếu dự án của bạn vẫn đang dùng CocoaPods, hãy xóa các pod `Adapty` và `AdaptyUI` khỏi `Podfile`, chạy `pod install` để dọn dẹp, sau đó thêm package trong Xcode qua **File → Add Package Dependency** với đường dẫn `https://github.com/adaptyteam/AdaptySDK-iOS.git`.
## Chế độ Kids Mode: sản phẩm riêng biệt được thay thế bằng package trait \{#kids-mode-separate-products-replaced-by-a-package-trait\}

Trong v3, bạn bật [Kids Mode](kids-mode) bằng cách chọn các sản phẩm package riêng biệt **Adapty_KidsMode** và **AdaptyUI_KidsMode** rồi đổi tên các import. Kể từ `4.0.0-beta.2`, các sản phẩm này đã bị loại bỏ. Kids Mode hiện là một Swift package trait có tên `KidsMode` trên package Adapty thông thường — khi bật lên, IDFA và AdSupport sẽ bị loại ra khỏi toàn bộ SDK trong quá trình biên dịch.

Để migrate:
1. Trong cửa sổ **Choose Package Products**, chọn sản phẩm **Adapty** và **AdaptyUI** thông thường thay vì **Adapty_KidsMode** và **AdaptyUI_KidsMode**.

2. Bật trait `KidsMode`. Trong Xcode 26.4 trở lên, bật trait này cho dependency AdaptySDK-iOS trong phần **Package Dependencies** của dự án. Nếu bạn thêm Adapty dưới dạng dependency trong `Package.swift` (yêu cầu `swift-tools-version` 6.1 trở lên), hãy bật tại đó:
```swift showLineNumbers title="Package.swift"
   .package(
       url: "https://github.com/adaptyteam/AdaptySDK-iOS.git",
       exact: "4.0.0-beta.2",
       traits: ["KidsMode"]
   )
   ```

3. Đổi lại các import về module thông thường:

   ```diff showLineNumbers
   - import Adapty_KidsMode
   - import AdaptyUI_KidsMode
   + import Adapty
   + import AdaptyUI
   ```
:::note
Các phiên bản Xcode trước 26.4 không thể bật trait cho dự án Xcode từ giao diện. Trong trường hợp đó, hãy thêm một Swift package cục bộ nhỏ phụ thuộc vào Adapty với trait `KidsMode` được bật, rồi để app target của bạn phụ thuộc vào package đó.
:::
## Các API đã bị xóa \{#removed-apis\}

- **`Adapty.getPaywallProductsWithoutDeterminingOffer(paywall:)`** — đã bị xóa. Tất cả sản phẩm hiện đã bao gồm thông tin ưu đãi, vì vậy không cần thêm bước kiểm tra tính đủ điều kiện riêng nữa.
- **`AdaptyPaywallProductWithoutDeterminingOffer`** — đã bị xóa. Các callback trước đây truyền kiểu này (chẳng hạn như `didSelectProduct`) nay sẽ truyền `AdaptyPaywallProduct`.
## Tính năng in-app purchase được quảng bá trên App Store tạm thời bị gỡ bỏ \{#app-store-promoted-in-app-purchases-temporarily-removed\}

Trong quá trình migrate sang StoreKit 2, Adapty iOS SDK 4.0 loại bỏ hỗ trợ cho in-app purchase được quảng bá trên App Store. Phương thức delegate `shouldAddStorePayment(for:)` và kiểu `AdaptyDeferredProduct` mà nó nhận đều không khả dụng trong phiên bản 4.0.

:::warning
Việc gỡ bỏ này chỉ là tạm thời — hỗ trợ in-app purchase được quảng bá sẽ trở lại trong một bản phát hành 4.x sau này. Nếu ứng dụng của bạn phụ thuộc vào tính năng này, hãy tiếp tục sử dụng iOS SDK 3.x cho đến khi hỗ trợ được khôi phục.
:::
## Lấy paywalls \{#fetching-paywalls\}
### getPaywall + getPaywallConfiguration → getFlow + getFlowConfiguration

Các kiểu trả về thay đổi từ `AdaptyPaywall` / `AdaptyUI.PaywallConfiguration` sang `AdaptyFlow` / `AdaptyUI.FlowConfiguration`. Tham số `locale` được chuyển ra khỏi lệnh gọi fetch và đưa vào `getFlowConfiguration`:
```diff showLineNumbers
- let paywall = try await Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en")
- let paywallConfiguration = try await AdaptyUI.getPaywallConfiguration(forPaywall: paywall)
+ let flow = try await Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID")
+ let flowConfiguration = try await AdaptyUI.getFlowConfiguration(forFlow: flow, locale: "en")
```

### getPaywallProducts(paywall:) → getPaywallProducts(flow:)

`getPaywallProducts` hiện nhận một `AdaptyFlow` được trả về bởi `Adapty.getFlow`:

```diff showLineNumbers
- let products = try await Adapty.getPaywallProducts(paywall: paywall)
+ let products = try await Adapty.getPaywallProducts(flow: flow)
```
## Theo dõi lượt xem paywall \{#tracking-paywall-views\}
### logShowPaywall(_:) → logShowFlow(_:)

`logShowPaywall` được đổi tên thành `logShowFlow` và nhận vào một `AdaptyFlow` thay vì `AdaptyPaywall`. Sự kiện vẫn được ghi lại theo cùng một biến thể, do đó các chỉ số funnel và A/B test hiện có vẫn hoạt động bình thường mà không cần thay đổi gì trên dashboard.

```diff showLineNumbers
- try await Adapty.logShowPaywall(paywall)
+ try await Adapty.logShowFlow(flow)
```
Như trong v3, bạn không cần gọi phương thức này khi hiển thị các flow hoặc paywall được render bởi [Flow Builder](adapty-flow-builder) hoặc [Paywall Builder](adapty-paywall-builder) — Adapty tự động theo dõi các lượt xem đó.
## didFinishPurchase hiện là bắt buộc \{#didfinishpurchase-is-now-required\}

Trong v3, `didFinishPurchase` là tùy chọn: nếu bạn không implement nó, paywall sẽ tự đóng sau khi mua hàng thành công. Bắt đầu từ `4.0.0-beta.2`, tính năng tự động đóng mặc định này đã bị loại bỏ để flow có thể tiếp tục sau khi mua hàng thành công — ví dụ: để hiển thị các màn hình còn lại trong flow của bạn. Bây giờ bạn tự quyết định điều gì xảy ra sau khi mua: đóng màn hình, hoặc không làm gì để flow tiếp tục.
- **UIKit**: Những đối tượng tuân theo `AdaptyFlowControllerDelegate` phải triển khai `didFinishPurchase` — phương thức này không còn có phần triển khai mặc định nữa.
- **SwiftUI**: closure `didFinishPurchase` của `.flow(...)` và `AdaptyFlowView(...)` giờ là bắt buộc (non-optional), giống với `didFailPurchase` và `didFinishRestore`.

Để giữ nguyên hành vi của v3, hãy tự dismiss màn hình:
```swift showLineNumbers title="Swift"
func flowController(
    _ controller: AdaptyFlowController,
    didFinishPurchase product: AdaptyPaywallProduct,
    purchaseResult: AdaptyPurchaseResult
) {
    if !purchaseResult.isPurchaseCancelled {
        controller.dismiss(animated: true)
    }
}
```

## UIKit \{#uikit\}
### AdaptyPaywallController → AdaptyFlowController

Đổi tên kiểu controller và factory method:

```diff showLineNumbers
- let controller = try AdaptyUI.paywallController(
-     with: paywallConfiguration,
-     delegate: self
- )
+ let controller = try AdaptyUI.flowController(
+     with: flowConfiguration,
+     delegate: self
+ )
```
### AdaptyPaywallControllerDelegate → AdaptyFlowControllerDelegate

Đổi tên protocol và cập nhật tất cả các method signature. Lưu ý rằng `didSelectProduct` hiện nhận `AdaptyPaywallProduct` thay vì `AdaptyPaywallProductWithoutDeterminingOffer` đã bị xóa, và `didFinishPurchase` [phải được implement](#didfinishpurchase-is-now-required) — nó không còn có default implementation nữa.
```diff showLineNumbers
- class YourClass: AdaptyPaywallControllerDelegate {
+ class YourClass: AdaptyFlowControllerDelegate {

-     func paywallControllerDidAppear(_ controller: AdaptyPaywallController) { }
+     func flowControllerDidAppear(_ controller: AdaptyFlowController) { }

-     func paywallControllerDidDisappear(_ controller: AdaptyPaywallController) { }
+     func flowControllerDidDisappear(_ controller: AdaptyFlowController) { }

-     func paywallController(_ controller: AdaptyPaywallController,
-                            didPerform action: AdaptyUI.Action) { }
+     func flowController(_ controller: AdaptyFlowController,
+                         didPerform action: AdaptyUI.Action) { }

-     func paywallController(_ controller: AdaptyPaywallController,
-                            didSelectProduct product: AdaptyPaywallProductWithoutDeterminingOffer) { }
+     func flowController(_ controller: AdaptyFlowController,
+                         didSelectProduct product: AdaptyPaywallProduct) { }

-     func paywallController(_ controller: AdaptyPaywallController,
-                            didStartPurchase product: AdaptyPaywallProduct) { }
+     func flowController(_ controller: AdaptyFlowController,
+                         didStartPurchase product: AdaptyPaywallProduct) { }

-     func paywallController(_ controller: AdaptyPaywallController,
-                            didFinishPurchase product: AdaptyPaywallProduct,
-                            purchaseResult: AdaptyPurchaseResult) { }
+     func flowController(_ controller: AdaptyFlowController,
+                         didFinishPurchase product: AdaptyPaywallProduct,
+                         purchaseResult: AdaptyPurchaseResult) { }

-     func paywallController(_ controller: AdaptyPaywallController,
-                            didFailPurchase product: AdaptyPaywallProduct,
-                            error: AdaptyError) { }
+     func flowController(_ controller: AdaptyFlowController,
+                         didFailPurchase product: AdaptyPaywallProduct,
+                         error: AdaptyError) { }

-     func paywallControllerDidStartRestore(_ controller: AdaptyPaywallController) { }
+     func flowControllerDidStartRestore(_ controller: AdaptyFlowController) { }

-     func paywallController(_ controller: AdaptyPaywallController,
-                            didFinishRestoreWith profile: AdaptyProfile) { }
+     func flowController(_ controller: AdaptyFlowController,
+                         didFinishRestoreWith profile: AdaptyProfile) { }

-     func paywallController(_ controller: AdaptyPaywallController,
-                            didFailRestoreWith error: AdaptyError) { }
+     func flowController(_ controller: AdaptyFlowController,
+                         didFailRestoreWith error: AdaptyError) { }

-     func paywallController(_ controller: AdaptyPaywallController,
-                            didFailRenderingWith error: AdaptyUIError) { }
+     func flowController(_ controller: AdaptyFlowController,
+                         didReceiveError error: AdaptyUIError) { }

-     func paywallController(_ controller: AdaptyPaywallController,
-                            didFailLoadingProductsWith error: AdaptyError) -> Bool { }
+     func flowController(_ controller: AdaptyFlowController,
+                         didFailLoadingProductsWith error: AdaptyError) -> Bool { }

-     func paywallController(_ controller: AdaptyPaywallController,
-                            didPartiallyLoadProducts failedIds: [String]) { }
+     func flowController(_ controller: AdaptyFlowController,
+                         didPartiallyLoadProducts failedIds: [String]) { }

-     func paywallController(_ controller: AdaptyPaywallController,
-                            didFinishWebPaymentNavigation product: AdaptyPaywallProduct?,
-                            error: AdaptyError?) { }
+     func flowController(_ controller: AdaptyFlowController,
+                         didFinishWebPaymentNavigation product: AdaptyPaywallProduct?,
+                         error: AdaptyError?) { }
 }
```

## SwiftUI \{#swiftui\}
### Modifier `.paywall()` → `.flow()` \{#paywall-modifier--flow\}

Đổi tên modifier, cập nhật tên tham số cấu hình, và thêm closure `didFinishPurchase` [bắt buộc](#didfinishpurchase-is-now-required):
```diff showLineNumbers
 @State var flowPresented = false // rename freely — the variable name is your choice

 var body: some View {
     Text("Hello, AdaptyUI!")
-        .paywall(
+        .flow(
             isPresented: $flowPresented,
-            paywallConfiguration: paywallConfiguration,
+            flowConfiguration: flowConfiguration,
+            didFinishPurchase: { product, purchaseResult in /* dismiss, or do nothing to let the flow continue */ },
             didFailPurchase: { product, error in /* handle the error */ },
             didFinishRestore: { profile in /* check access level and dismiss */ },
             didFailRestore: { error in /* handle the error */ },
-            didFailRendering: { error in flowPresented = false }
+            didReceiveError: { error in flowPresented = false }
         )
 }
```

Callback được đổi tên này kích hoạt cho những lỗi render giống như `didFailRendering` trước đây, cộng thêm các lỗi runtime mới từ flow script (JavaScript exception với mã `AdaptyUIError` là `4105` — `.jsException`). Phần thân handler hiện có không cần thay đổi code — chỉ cần đổi tên tham số.
### AdaptyPaywallView → AdaptyFlowView

Đổi tên view, cập nhật tham số cấu hình, thêm closure `didFinishPurchase` [bắt buộc](#didfinishpurchase-is-now-required), và cập nhật closure `didSelectProduct` nếu có — giờ đây nó nhận `AdaptyPaywallProduct` thay cho `AdaptyPaywallProductWithoutDeterminingOffer` đã bị xóa:
```diff showLineNumbers
- AdaptyPaywallView(
-     paywallConfiguration: paywallConfiguration,
-     didSelectProduct: { product: AdaptyPaywallProductWithoutDeterminingOffer in /* handle */ },
+ AdaptyFlowView(
+     flowConfiguration: flowConfiguration,
+     didSelectProduct: { product: AdaptyPaywallProduct in /* handle */ },
+     didFinishPurchase: { product, purchaseResult in /* dismiss, or do nothing to let the flow continue */ },
     didFailPurchase: { product, error in /* handle the error */ },
     didFinishRestore: { profile in /* check access level and dismiss */ },
     didFailRestore: { error in /* handle the error */ },
-    didFailRendering: { error in /* handle the error */ }
+    didReceiveError: { error in /* handle the error */ }
 )
```

## Tài nguyên tùy chỉnh của AdaptyUI \{#adaptyui-custom-assets\}
### AdaptyUICustomVideoAsset

Có hai thay đổi ảnh hưởng đến tất cả các call site hiện có:

- `.player` hiện nhận `AVPlayer` thay vì `AVQueuePlayer`.
- Mỗi case đều có thêm tham số `resolution: CGSize?` ở cuối. Truyền `nil` để giữ nguyên hành vi hiện tại, hoặc truyền kích thước pixel thực tế để player có thể dành chỗ bố cục (tỉ lệ khung hình = `width / height`) trước khi video tải xong.
```diff showLineNumbers
- case file(url: URL, preview: AdaptyUICustomImageAsset?)
- case remote(url: URL, preview: AdaptyUICustomImageAsset?)
- case player(item: AVPlayerItem, player: AVQueuePlayer, preview: AdaptyUICustomImageAsset?)
+ case file(url: URL, preview: AdaptyUICustomImageAsset?, resolution: CGSize?)
+ case remote(url: URL, preview: AdaptyUICustomImageAsset?, resolution: CGSize?)
+ case player(item: AVPlayerItem, player: AVPlayer, preview: AdaptyUICustomImageAsset?, resolution: CGSize?)
```

## Định danh attribution và tích hợp \{#attribution-and-integration-identifiers\}
### updateAttribution(_:source:)

Tham số `source` thay đổi từ kiểu `String` sang kiểu `AdaptyAttributionSource` mới, và `AdaptyProfile.AttributionSource` trước đây được đặt lồng nhau nay được đổi tên thành `AdaptyAttributionSource` ở cấp độ top-level. Sử dụng một trong các source được định nghĩa sẵn, hoặc truyền một string literal cho bất kỳ source nào khác — `AdaptyAttributionSource` tuân theo `ExpressibleByStringLiteral`, vì vậy các lệnh gọi dùng string literal hiện có vẫn tiếp tục biên dịch được.
```diff showLineNumbers
- try await Adapty.updateAttribution(attribution, source: "adjust")
+ try await Adapty.updateAttribution(attribution, source: .adjust)
```

Nguồn được định sẵn: `.appleAds`, `.adjust`, `.appsflyer`, `.branch`, `.tenjin`. Nếu bạn lưu nguồn trong biến `String`, hãy bọc nó lại: `AdaptyAttributionSource(rawValue: yourSource)`.
### setIntegrationIdentifier(_:)

`setIntegrationIdentifier(key:value:)` được thay thế bằng một phương thức variadic nhận một hoặc nhiều giá trị `AdaptyIntegrationIdentifier`. Hãy dùng các factory method có sẵn thay vì chuỗi key thô:

```diff showLineNumbers
- try await Adapty.setIntegrationIdentifier(key: "appsflyer_id", value: uid)
+ try await Adapty.setIntegrationIdentifier(.appsflyerId(uid))
```

Bạn có thể đặt nhiều identifier trong một lần gọi:

```swift showLineNumbers
try await Adapty.setIntegrationIdentifier(
    .appsflyerId(uid),
    .adjustDeviceId(adid)
)
```
Thay thế từng chuỗi key cũ bằng factory method tương ứng:
| v3 key | v4 factory |
|---|---|
| `"adjust_device_id"` | `.adjustDeviceId(_:)` |
| `"airbridge_device_id"` | `.airbridgeDeviceId(_:)` |
| `"amplitude_user_id"` | `.amplitudeUserId(_:)` |
| `"amplitude_device_id"` | `.amplitudeDeviceId(_:)` |
| `"appmetrica_device_id"` | `.appmetricaDeviceId(_:)` |
| `"appmetrica_profile_id"` | `.appmetricaProfileId(_:)` |
| `"appsflyer_id"` | `.appsflyerId(_:)` |
| `"branch_id"` | `.branchId(_:)` |
| `"facebook_anonymous_id"` | `.facebookAnonymousId(_:)` |
| `"firebase_app_instance_id"` | `.firebaseAppInstanceId(_:)` |
| `"mixpanel_user_id"` | `.mixpanelUserId(_:)` |
| `"one_signal_subscription_id"` | `.oneSignalSubscriptionId(_:)` |
| `"one_signal_player_id"` | `.oneSignalPlayerId(_:)` |
| `"posthog_distinct_user_id"` | `.posthogDistinctUserId(_:)` |
| `"pushwoosh_hwid"` | `.pushwooshHWID(_:)` |
| `"tenjin_analytics_installation_id"` | `.tenjinAnalyticsInstallationId(_:)` |