Migrate Adapty iOS SDK sang v4.0

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

v3v4
Adapty.getPaywall(placementId:locale:)Adapty.getFlow(placementId:)
AdaptyUI.getPaywallConfiguration(forPaywall:)AdaptyUI.getFlowConfiguration(forFlow:locale:)
Adapty.getPaywallProducts(paywall:)Adapty.getPaywallProducts(flow:)
Adapty.logShowPaywall(_:)Adapty.logShowFlow(_:)
AdaptyPaywallControllerAdaptyFlowController
AdaptyPaywallControllerDelegateAdaptyFlowControllerDelegate
AdaptyUI.paywallController(with:delegate:)AdaptyUI.flowController(with:delegate:)
.paywall() (SwiftUI modifier).flow()
AdaptyPaywallViewAdaptyFlowView
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_KidsModePackage 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

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ợ

Adapty iOS SDK 4.0 không còn hỗ trợ CocoaPods. Hãy cài đặt SDK bằng Swift Package Manager.

Nếu dự án của bạn vẫn đang dùng CocoaPods, hãy xóa các pod AdaptyAdaptyUI 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

Trong v3, bạn bật Kids Mode bằng cách chọn các sản phẩm package riêng biệt Adapty_KidsModeAdaptyUI_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 AdaptyAdaptyUI thông thường thay vì Adapty_KidsModeAdaptyUI_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 đó:

   .package(
       url: "https://github.com/adaptyteam/AdaptySDK-iOS.git",
       exact: "4.0.0-beta.2",
       traits: ["KidsMode"]
   )
  1. Đổi lại các import về module thông thường:

    - import Adapty_KidsMode
    - import AdaptyUI_KidsMode
    + import Adapty
    + import AdaptyUI

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

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

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.

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

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:

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

- let products = try await Adapty.getPaywallProducts(paywall: paywall)
+ let products = try await Adapty.getPaywallProducts(flow: flow)

Theo dõi lượt xem paywall

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.

- 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 hoặc Paywall Builder — Adapty tự động theo dõi các lượt xem đó.

didFinishPurchase hiện là bắt buộc

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(...)AdaptyFlowView(...) giờ là bắt buộc (non-optional), giống với didFailPurchasedidFinishRestore.

Để giữ nguyên hành vi của v3, hãy tự dismiss màn hình:

func flowController(
    _ controller: AdaptyFlowController,
    didFinishPurchase product: AdaptyPaywallProduct,
    purchaseResult: AdaptyPurchaseResult
) {
    if !purchaseResult.isPurchaseCancelled {
        controller.dismiss(animated: true)
    }
}

UIKit

AdaptyPaywallController → AdaptyFlowController

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

- 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 — nó không còn có default implementation nữa.

- 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

Modifier .paywall().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:

 @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ã AdaptyUIError4105.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, và cập nhật closure didSelectProduct nếu có — giờ đây nó nhận AdaptyPaywallProduct thay cho AdaptyPaywallProductWithoutDeterminingOffer đã bị xóa:

- 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

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

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.

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

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

try await Adapty.setIntegrationIdentifier(
    .appsflyerId(uid),
    .adjustDeviceId(adid)
)

Thay thế từng chuỗi key cũ bằng factory method tương ứng:

v3 keyv4 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(_:)