# IOS - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Locale: vi Generated on: 2026-07-01T16:30:13.653Z Total files: 44 --- # File: sdk-installation-ios --- --- title: "Cài đặt & cấu hình iOS SDK" description: "Hướng dẫn từng bước cài đặt Adapty SDK trên iOS cho các ứng dụng dựa trên gói đăng ký." --- Adapty SDK bao gồm hai module chính để tích hợp vào ứng dụng của bạn: - **Core Adapty**: Module SDK cốt lõi, bắt buộc phải có để Adapty hoạt động đúng trong ứng dụng. - **AdaptyUI**: Module tùy chọn, cần thiết nếu bạn sử dụng [Adapty Paywall Builder](adapty-paywall-builder) — công cụ no-code thân thiện giúp tạo paywall đa nền tảng dễ dàng. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng? Hãy xem [ứng dụng mẫu](https://github.com/adaptyteam/AdaptySDK-iOS/tree/master/Examples) của chúng tôi, minh họa toàn bộ quá trình thiết lập bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: Để xem hướng dẫn triển khai đầy đủ, bạn cũng có thể xem các video sau:
## Yêu cầu \{#requirements\} Adapty iOS SDK yêu cầu iOS 15.0 trở lên. :::important Adapty SDK 3.15.7+ là bắt buộc khi build với Xcode 26.4 trở lên. ::: --- no_index: true --- import Callout from '../../../components/Callout.astro'; Cài đặt SDK là bước 5 trong quá trình thiết lập Adapty. Trước khi các giao dịch mua hàng hoạt động trong ứng dụng, bạn cần kết nối ứng dụng với các cửa hàng, sau đó tạo sản phẩm, paywall và placement trong Adapty Dashboard. [Hướng dẫn quickstart](quickstart) sẽ hướng dẫn bạn qua tất cả các bước cần thiết. ## Cài đặt Adapty SDK \{#install-adapty-sdk\} [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-iOS.svg?style=flat&logo=apple)](https://github.com/adaptyteam/AdaptySDK-iOS/releases) Adapty SDK được cài đặt qua Swift Package Manager. Trong Xcode, vào **File** -> **Add Package Dependency...**. Lưu ý rằng các bước thêm package dependency có thể khác nhau tùy phiên bản Xcode, vì vậy hãy tham khảo tài liệu Xcode nếu cần. 1. Nhập URL repository: ``` https://github.com/adaptyteam/AdaptySDK-iOS.git ``` 2. Chọn phiên bản (khuyến nghị chọn phiên bản ổn định mới nhất) và nhấn **Add Package**. 3. Trong cửa sổ **Choose Package Products**, chọn các module bạn cần: - **Adapty** (module cốt lõi) - **AdaptyUI** (tùy chọn - chỉ khi bạn dự định sử dụng Paywall Builder) :::note Lưu ý: - Để bật [chế độ Kids](kids-mode), hãy chọn **Adapty_KidsMode** thay vì **Adapty**. - Không chọn các package khác trong danh sách – bạn sẽ không cần chúng. ::: 4. Nhấn **Add Package** để hoàn tất cài đặt. 5. **Kiểm tra cài đặt:** Trong project navigator, bạn sẽ thấy "Adapty" (và "AdaptyUI" nếu đã chọn) dưới mục **Package Dependencies**. :::important Adapty iOS SDK 4.0 là phiên bản pre-release. Swift Package Manager không tự động resolve các phiên bản beta qua quy tắc **Up to Next Major Version** (`from:`), vì vậy bạn phải chỉ định chính xác phiên bản. Trong Xcode, đặt **Dependency Rule** thành **Exact Version** và nhập `4.0.0-beta.1`. Trong `Package.swift`, dùng `.exact("4.0.0-beta.1")`. Xem [Migrate Adapty iOS SDK lên v4](migration-to-ios-sdk-v4). ::: ## Kích hoạt module Adapty của Adapty SDK \{#activate-adapty-module-of-adapty-sdk\} Kích hoạt Adapty SDK trong code của ứng dụng. :::note Adapty SDK chỉ cần được kích hoạt một lần trong ứng dụng của bạn. ::: Để lấy **Public SDK Key**: 1. Truy cập Adapty Dashboard và điều hướng đến [**App settings → General**](https://app.adapty.io/settings/general). 2. Trong phần **Api keys**, sao chép **Public SDK Key** (KHÔNG phải Secret Key). 3. Thay thế `"YOUR_PUBLIC_SDK_KEY"` trong code. Hoặc lấy theo cách lập trình, sử dụng [Adapty CLI](developer-cli): ``` npm install -g adapty adapty auth login adapty apps list ``` Hoặc, trực tiếp: ``` npx adapty auth login adapty apps list ``` - Đảm bảo bạn sử dụng **Public SDK key** để khởi tạo Adapty, **Secret key** chỉ nên dùng cho [server-side API](getting-started-with-server-side-api). - **SDK keys** là duy nhất cho mỗi ứng dụng, vì vậy nếu bạn có nhiều ứng dụng, hãy đảm bảo chọn đúng key. ```swift showLineNumbers @main struct YourApp: App { init() { // Configure Adapty SDK let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") // Get from Adapty dashboard Adapty.logLevel = .verbose // recommended for development and the first production release let config = configurationBuilder.build() // Activate Adapty SDK asynchronously Task { do { try await Adapty.activate(with: config) } catch { // Handle error appropriately for your app print("Adapty activation failed: ", error) } } var body: some Scene { WindowGroup { // Your content view } } } } ``` ```swift showLineNumbers // In your AppDelegate class: // If you only use an AppDelegate, place the following code in the // application(_:didFinishLaunchingWithOptions:) method. // If you use a SceneDelegate, place the following code in the // scene(_:willConnectTo:options:) method. Task { do { let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") // Get from Adapty dashboard .with(logLevel: .verbose) // recommended for development and the first production release let config = configurationBuilder.build() try await Adapty.activate(with: config) } catch { // Handle error appropriately for your app print("Adapty activation failed: ", error) } } ``` :::important Hãy chờ `activate` hoàn thành trước khi gọi bất kỳ phương thức nào khác của Adapty SDK. Xem [Thứ tự gọi trong iOS SDK](ios-sdk-call-order) để biết trình tự đầy đủ. ::: Tiếp theo, hãy thiết lập paywall trong ứng dụng: - Nếu bạn dùng [Adapty Paywall Builder](adapty-paywall-builder), trước tiên hãy [kích hoạt module AdaptyUI](#activate-adaptyui-module-of-adapty-sdk) bên dưới, sau đó làm theo [hướng dẫn nhanh Paywall Builder](ios-quickstart-paywalls). - Nếu bạn tự xây dựng giao diện paywall, xem [hướng dẫn nhanh cho paywall tùy chỉnh](ios-quickstart-manual). ## Kích hoạt module AdaptyUI của Adapty SDK \{#activate-adaptyui-module-of-adapty-sdk\} Nếu bạn dự định dùng [Paywall Builder](adapty-paywall-builder) và đã [cài đặt module AdaptyUI](sdk-installation-ios#install-adapty-sdk), bạn cũng cần kích hoạt AdaptyUI. :::important Trong code, bạn phải kích hoạt module Adapty cốt lõi trước khi kích hoạt AdaptyUI. ::: ```swift showLineNumbers title="Swift" @main struct YourApp: App { init() { // ...ConfigurationBuilder steps // Activate Adapty SDK asynchronously Task { do { try await Adapty.activate(with: config) try await AdaptyUI.activate() } catch { // Handle error appropriately for your app print("Adapty activation failed: ", error) } } // main body... } } ``` ```swift showLineNumbers title="UIKit" // If you only use an AppDelegate, place the following code in the // application(_:didFinishLaunchingWithOptions:) method. // If you use a SceneDelegate, place the following code in the // scene(_:willConnectTo:options:) method. Task { do { let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") // Get from Adapty dashboard .with(logLevel: .verbose) // recommended for development let config = configurationBuilder.build() try await Adapty.activate(with: config) try await AdaptyUI.activate() } catch { // Handle error appropriately for your app print("Adapty activation failed: ", error) } } ``` :::tip Tùy chọn, khi kích hoạt AdaptyUI, bạn có thể [ghi đè cài đặt cache mặc định cho paywall](#set-up-media-cache-configuration-for-adaptyui). ::: ## Cài đặt tùy chọn \{#optional-setup\} ### Logging \{#logging\} #### Thiết lập hệ thống logging \{#set-up-the-logging-system\} Adapty ghi log các lỗi và thông tin quan trọng khác để giúp bạn hiểu những gì đang xảy ra. Các cấp độ log có sẵn: | Cấp độ | Mô tả | | ---------- | ------------------------------------------------------------ | | `error` | Chỉ ghi log các lỗi | | `warn` | Ghi log các lỗi và thông báo từ SDK không gây ra lỗi nghiêm trọng nhưng đáng chú ý | | `info` | Ghi log các lỗi, cảnh báo và các thông báo thông tin khác nhau | | `verbose` | Ghi log mọi thông tin bổ sung có thể hữu ích trong quá trình debug, chẳng hạn như lời gọi hàm, API query, v.v. | ```swift showLineNumbers let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") .with(logLevel: .verbose) // recommended for development ``` #### Chuyển hướng thông báo từ hệ thống logging \{#redirect-the-logging-system-messages\} Nếu bạn cần gửi log của Adapty đến hệ thống của mình hoặc lưu vào file, hãy dùng phương thức `setLogHandler` và triển khai logic logging tùy chỉnh bên trong. Handler này nhận các bản ghi log chứa nội dung thông báo và cấp độ nghiêm trọng. ```swift showLineNumbers title="Swift" Adapty.setLogHandler { record in writeToLocalFile("Adapty \(record.level): \(record.message)") } ``` ### Chính sách dữ liệu \{#data-policies\} Adapty không lưu trữ dữ liệu cá nhân của người dùng trừ khi bạn chủ động gửi, nhưng bạn có thể áp dụng thêm các chính sách bảo mật dữ liệu để tuân thủ hướng dẫn của cửa hàng hoặc quy định của từng quốc gia. #### Tắt thu thập và chia sẻ IDFA \{#disable-idfa-collection-and-sharing\} Khi kích hoạt module Adapty, đặt `idfaCollectionDisabled` thành `true` để tắt thu thập và chia sẻ IDFA. Dùng tham số này để tuân thủ App Store Review Guidelines hoặc tránh kích hoạt lời nhắc App Tracking Transparency khi IDFA không cần thiết cho ứng dụng của bạn. Giá trị mặc định là `false`. Để biết thêm chi tiết về thu thập IDFA, tham khảo phần [Tích hợp Analytics](analytics-integration#disable-collection-of-advertising-identifiers). ```swift showLineNumbers let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") .with(idfaCollectionDisabled: true) ``` #### Tắt thu thập và chia sẻ địa chỉ IP \{#disable-ip-collection-and-sharing\} Khi kích hoạt module Adapty, đặt `ipAddressCollectionDisabled` thành `true` để tắt thu thập và chia sẻ địa chỉ IP của người dùng. Giá trị mặc định là `false`. Dùng tham số này để tăng cường quyền riêng tư của người dùng, tuân thủ các quy định bảo vệ dữ liệu theo khu vực (như GDPR hoặc CCPA), hoặc giảm thu thập dữ liệu không cần thiết khi các tính năng dựa trên IP không bắt buộc trong ứng dụng của bạn. ```swift showLineNumbers let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") .with(ipAddressCollectionDisabled: true) ``` #### Cấu hình media cache cho paywall trong AdaptyUI \{#set-up-media-cache-configuration-for-adaptyui\} Lưu ý rằng cấu hình AdaptyUI là tùy chọn. Bạn có thể kích hoạt module AdaptyUI mà không cần config. Tuy nhiên, nếu bạn dùng config, tất cả các tham số đều bắt buộc. ```swift showLineNumbers title="Swift" // Configure AdaptyUI let adaptyUIConfiguration = AdaptyUI.Configuration( mediaCacheConfiguration: .init( memoryStorageTotalCostLimit: 100 * 1024 * 1024, memoryStorageCountLimit: .max, diskStorageSizeLimit: 100 * 1024 * 1024 ) ) // Activate AdaptyUI AdaptyUI.activate(configuration: adaptyUIConfiguration) ``` Tham số: | Tham số | Bắt buộc | Mô tả | | :-------------------------- | :------- | :----------------------------------------------------------- | | memoryStorageTotalCostLimit | bắt buộc | Tổng giới hạn chi phí lưu trữ tính bằng byte. | | memoryStorageCountLimit | bắt buộc | Giới hạn số lượng item của bộ nhớ lưu trữ. | | diskStorageSizeLimit | bắt buộc | Giới hạn kích thước file trên đĩa của bộ nhớ lưu trữ tính bằng byte. Giá trị 0 nghĩa là không giới hạn. | ### Hành vi hoàn tất giao dịch \{#transaction-finishing-behavior\} :::info Tính năng này có sẵn từ SDK phiên bản 3.12.0 trở lên. ::: Mặc định, Adapty tự động hoàn tất giao dịch sau khi xác thực thành công. Tuy nhiên, nếu bạn cần xác thực giao dịch nâng cao (như xác thực receipt phía server, phát hiện gian lận hoặc logic nghiệp vụ tùy chỉnh), bạn có thể cấu hình SDK để hoàn tất giao dịch thủ công. ```swift showLineNumbers title="Swift" let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") .with(transactionsFinishBehavior: .manual) // .auto is the default ``` Xem thêm chi tiết về cách hoàn tất giao dịch trong [hướng dẫn](ios-transaction-management). ### Xóa dữ liệu khi khôi phục từ backup \{#clear-data-on-backup-restore\} Khi `clearDataOnBackup` được đặt thành `true`, SDK sẽ phát hiện khi ứng dụng được khôi phục từ backup iCloud và xóa toàn bộ dữ liệu SDK được lưu trữ cục bộ, bao gồm thông tin hồ sơ người dùng được cache, chi tiết sản phẩm và paywall. Sau đó SDK sẽ khởi tạo lại với trạng thái sạch. Giá trị mặc định là `false`. :::note Chỉ xóa cache SDK cục bộ. Lịch sử giao dịch với Apple và dữ liệu người dùng trên server Adapty vẫn được giữ nguyên. ::: ```swift showLineNumbers let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") .with(clearDataOnBackup: true) // default – false ``` ## Xử lý sự cố \{#troubleshooting\} #### Lỗi Swift 6 concurrency với Tuist \{#swift-6-concurrency-error-with-tuist\} Khi build với [Tuist](https://tuist.dev/), bạn có thể gặp lỗi biên dịch Swift 6 strict concurrency. Triệu chứng điển hình bao gồm lỗi không khớp thuộc tính `@Sendable` trong `AdaptyUIBuilderLogic` hoặc các lỗi Sendability cross-module tương tự. Nguyên nhân là Tuist tạo project Xcode từ các SPM package nhưng không giữ lại cài đặt `swift-tools-version: 6.0`. Kết quả là một số target Adapty (`Adapty`, `AdaptyUI`, `AdaptyUIBuilder`) biên dịch với quy tắc Swift 5 trong khi các target khác dùng Swift 6, tạo ra lỗi `@Sendable` cross-module. **Cách khắc phục**: Nâng cấp lên Adapty SDK **3.15.5** trở lên, phiên bản này giải quyết vấn đề bất kể phiên bản ngôn ngữ Swift hỗn hợp. **Cách tạm thời**: Nếu bạn không thể nâng cấp, hãy đặt rõ Swift 6 cho cả ba target Adapty trong cấu hình Tuist: ```swift showLineNumbers targetSettings: [ "Adapty": .init().swiftVersion("6"), "AdaptyUI": .init().swiftVersion("6"), "AdaptyUIBuilder": .init().swiftVersion("6"), ] ``` --- # File: ios-quickstart-paywalls --- --- title: "Bật tính năng mua hàng với Flow Builder trong iOS SDK" description: "Hướng dẫn nhanh để bật in-app purchase với Adapty Flow Builder." --- Để bật tính năng in-app purchase, bạn cần hiểu ba khái niệm chính: - [**Sản phẩm**](product) – bất cứ thứ gì người dùng có thể mua (gói đăng ký, consumable, quyền truy cập trọn đời) - [**Flow**](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 no-code. SDK lấy chúng qua `getFlow`. Nếu bạn muốn tự xây dựng giao diện trong code, hãy dùng paywall thay thế — xem [Triển khai paywall thủ công](ios-quickstart-manual). - [**Placement**](placements) – vị trí và thời điểm bạn hiển thị flow trong app (ví dụ: `main`, `onboarding`, `settings`). Bạn gắn flow vào placement trong dashboard, sau đó lấy chúng theo placement ID trong code. Cách này giúp 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 ba cách để bật tính năng mua hàng trong app. Chọn một trong số đó tùy theo yêu cầu của app bạn: | Cách triển khai | Độ phức tạp | Khi nào sử dụng | |---|---|---| | Adapty Flow Builder | ✅ Dễ | Bạn [tạo một flow hoàn chỉnh, sẵn sàng thanh toán trong no-code builder](quickstart-paywalls). Adapty tự động render và xử lý toàn bộ quy trình mua hàng, xác thực receipt, và quản lý gói đăng ký ở phía sau. | | Paywall tự tạo | 🟡 Trung bình | Bạn tự triển khai giao diện paywall trong code app, nhưng vẫn lấy đối tượng flow từ Adapty để linh hoạt trong việc cung cấp sản phẩm. Xem [hướng dẫn](ios-quickstart-manual). | | Observer mode | 🔴 Khó | Bạn đã có sẵn hệ thống xử lý mua hàng riêng và muốn tiếp tục sử dụng nó. Lưu ý rằng observer mode có những hạn chế nhất định 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, 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 app của bạn, bạn chỉ cần: 1. **Lấy flow**: Lấy từ Adapty. 2. **Hiển thị và Adapty sẽ xử lý mua hàng cho bạn**: Hiển thị view trong app. 3. **Xử lý hành động nút bấm**: Liên kết tương tác của người dùng với phản hồi tương ứng trong app. 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 app của bạn 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 Adapty SDK](sdk-installation-ios) trong code app của bạn. Hướng dẫn này sử dụng API của Adapty iOS SDK v4 (beta). ## 1. Lấy flow \{#1-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 được tạo trong Adapty Flow Builder, bạn cần: 1. Lấy đối tượng `flow` theo [placement](placements) ID 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`. Nó chứa các phần tử giao diện và style 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\} Sau khi có cấu hình flow, bạn chỉ cần thêm vài dòng code để hiển thị flow. Trong SwiftUI, khi hiển thị flow, bạn cũng cần xử lý các sự kiện. `didFailPurchase`, `didFinishRestore`, `didFailRestore`, và `didReceiveError` là bắt buộc. Khi kiểm thử, bạn có thể chỉ cần sao chép code từ đoạn snippet dưới đây để ghi log các sự kiện này. :::tip Xử lý `didFinishPurchase` không bắt buộc, nhưng hữu ích khi bạn muốn thực hiện hành động sau khi mua hàng thành công. Nếu bạn không triển khai callback đó, flow sẽ tự động đóng lại. ::: ```swift .flow( isPresented: $flowPresented, flowConfiguration: flowConfiguration, 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)") } ) ``` ```swift func presentFlow(with config: AdaptyUI.FlowConfiguration) { let flowController = try AdaptyUI.flowController( with: config, delegate: self ) present(flowController, animated: true) } ``` Triển khai `AdaptyFlowControllerDelegate` để xử lý các sự kiện. Tối thiểu, hãy triển khai các error handler bắt buộc (ba phương thức không có implementation mặc định): ```swift extension YourViewController: AdaptyFlowControllerDelegate { 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)") } } ``` :::info Để biết thêm chi tiết về cách hiển thị flow, xem [hướng dẫn](ios-present-paywalls) của chúng tôi. ::: ## 3. Xử lý hành động nút bấm \{#3-handle-button-actions\} Khi người dùng nhấn 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, các nút khác có ID tùy chỉnh hoặc được định nghĩa trước và yêu cầu 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ụ, đây là cách xử lý nút đóng. Trong UIKit, SDK tự động đóng 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` thành `false`. :::tip Đọc cá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 bấm. ::: ```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 } }, 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 } ) ``` ```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 } } } ``` ## Các bước tiếp theo \{#next-steps\} --- no_index: true --- import Callout from '../../../components/Callout.astro'; 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\} Đây là cách tất cả các bước trong hướng dẫn này có thể được tích hợp vào app của bạn cùng nhau. ```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 } }, 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)") } } } ``` ```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, 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) } } ``` --- # File: ios-check-subscription-status --- --- title: "Kiểm tra trạng thái gói đăng ký trong iOS SDK" description: "Tìm hiểu cách kiểm tra trạng thái gói đăng ký trong ứng dụng iOS của bạn với Adapty." --- Để quyết định người dùng có thể truy cập nội dung trả phí hay cần hiển thị paywall, bạn cần kiểm tra [mức độ truy cập](access-level) của họ trong hồ sơ người dùng. Bài viết này hướng dẫn bạn cách truy cập trạng thái hồ sơ người dùng để quyết định nên hiển thị paywall hay cấp quyền truy cập tính năng trả phí cho người dùng. ## Lấy trạng thái gói đăng ký \{#get-subscription-status\} Khi quyết định hiển thị paywall hay nội dung trả phí cho người dùng, bạn kiểm tra [mức độ truy cập](access-level) trong hồ sơ người dùng của họ. Có hai cách: - Gọi `getProfile` nếu bạn cần dữ liệu hồ sơ mới nhất ngay lập tức (ví dụ khi khởi động ứng dụng) hoặc muốn buộc cập nhật. - Thiết lập **cập nhật hồ sơ tự động** để duy trì bản sao cục bộ được tự động làm mới mỗi khi trạng thái gói đăng ký thay đổi. :::important Theo mặc định, mức độ truy cập `premium` đã tồn tại trong Adapty. Nếu bạn không cần thiết lập nhiều hơn một mức độ truy cập, bạn có thể dùng `premium` luôn. ::: ### Lấy hồ sơ người dùng \{#get-profile\} Cách đơn giản nhất để lấy trạng thái gói đăng ký là dùng phương thức `getProfile` để truy cập hồ sơ người dùng: ```swift showLineNumbers do { let profile = try await Adapty.getProfile() if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // grant access to premium features } } catch { // handle the error } ``` ```swift showLineNumbers Adapty.getProfile { result in if let profile = try? result.get() { // check the access profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // grant access to premium features } } } ``` ### Lắng nghe cập nhật gói đăng ký \{#listen-to-subscription-updates\} Nếu bạn muốn tự động nhận cập nhật hồ sơ trong ứng dụng: 1. Conform vào protocol `AdaptyDelegate` trong kiểu dữ liệu bạn chọn và implement phương thức `didLoadLatestProfile` — Adapty sẽ tự động gọi phương thức này mỗi khi trạng thái gói đăng ký của người dùng thay đổi. Trong ví dụ dưới đây, chúng ta dùng kiểu `SubscriptionManager` để hỗ trợ xử lý các luồng gói đăng ký và hồ sơ người dùng. Kiểu này có thể được inject như một dependency hoặc thiết lập như singleton trong ứng dụng UIKit, hoặc thêm vào môi trường SwiftUI từ struct chính của ứng dụng. 2. Lưu dữ liệu hồ sơ được cập nhật khi phương thức này được gọi, để bạn có thể sử dụng xuyên suốt ứng dụng mà không cần thực hiện thêm request mạng. ```swift class SubscriptionManager: AdaptyDelegate { nonisolated func didLoadLatestProfile(_ profile: AdaptyProfile) { let hasAccess = profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false // Update UI, unlock content, etc. } } // Set delegate after Adapty activation Adapty.delegate = subscriptionManager ``` :::note Adapty tự động gọi `didLoadLatestProfile` khi ứng dụng khởi động, cung cấp dữ liệu gói đăng ký đã cache ngay cả khi thiết bị ngoại tuyến. ::: ## Kết nối hồ sơ người dùng với logic paywall \{#connect-profile-with-paywall-logic\} Khi bạn cần đưa ra quyết định ngay lập tức về việc hiển thị paywall hay cấp quyền truy cập tính năng trả phí, bạn có thể kiểm tra hồ sơ người dùng trực tiếp. Cách này hữu ích trong các tình huống như khởi động ứng dụng, khi vào các khu vực premium, hoặc trước khi hiển thị nội dung cụ thể. ```swift private func checkAccessLevel() async -> Bool { do { let profile = try await Adapty.getProfile() return profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false } catch { print("Error checking access level: \(error)") return false } } // In your initialization logic: let hasAccess = await checkAccessLevel() if !hasAccess { paywallPresented = true // Show paywall if no access } ``` ```swift private func checkAccessLevel() async throws -> Bool { let profile = try await Adapty.getProfile() return profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false } // In your initialization logic: let hasAccess = try await checkAccessLevel() if !hasAccess { presentPaywall(with: paywallConfiguration) } ``` ## Bước tiếp theo \{#next-steps\} Bây giờ bạn đã biết cách theo dõi trạng thái gói đăng ký, hãy [tìm hiểu cách làm việc với hồ sơ người dùng](ios-quickstart-identify) để đảm bảo nó phù hợp với hệ thống xác thực hiện có và quyền chia sẻ quyền truy cập trả phí của bạn. Nếu bạn không có hệ thống xác thực riêng, điều đó hoàn toàn không sao — Adapty sẽ quản lý người dùng cho bạn, nhưng bạn vẫn có thể đọc [hướng dẫn](ios-quickstart-identify) để tìm hiểu cách Adapty hoạt động với người dùng ẩn danh. --- # File: ios-quickstart-identify --- --- title: "Xác định người dùng trong iOS SDK" description: "Hướng dẫn nhanh để thiết lập Adapty cho việc quản lý gói đăng ký trong ứng dụng." --- :::important Hướng dẫn này dành cho bạn nếu bạn có hệ thống xác thực riêng. Ở đây, bạn sẽ học cách làm việc với hồ sơ người dùng trong Adapty để đảm bảo nó phù hợp với hệ thống xác thực hiện có của bạn. ::: Cách bạn quản lý các giao dịch mua của người dùng phụ thuộc vào mô hình xác thực của ứng dụng: - Nếu ứng dụng của bạn không sử dụng xác thực backend và không lưu trữ dữ liệu người dùng, xem [phần về người dùng ẩn danh](#anonymous-users). - Nếu ứng dụng của bạn có (hoặc sẽ có) xác thực backend, xem [phần về người dùng đã xác định](#identified-users). **Các khái niệm chính**: - **Hồ sơ người dùng** là các thực thể cần thiết để SDK hoạt động. Adapty tự động tạo chúng. - Chúng có thể là ẩn danh **(không có customer user ID)** hoặc đã xác định **(có customer user ID)**. - Bạn cung cấp **customer user ID** để liên kết chéo hồ sơ người dùng trong Adapty với hệ thống xác thực nội bộ của bạn. Dưới đây là sự khác biệt giữa người dùng ẩn danh và người dùng đã xác định: | | Người dùng ẩn danh | Người dùng đã xác định | |-------------------------|--------------------------------------------------------------|--------------------------------------------------------------------------------------| | **Quản lý mua hàng** | Khôi phục giao dịch mua ở cấp độ cửa hàng | Duy trì lịch sử mua hàng trên nhiều thiết bị thông qua customer user ID của họ | | **Quản lý hồ sơ** | Hồ sơ mới mỗi lần cài đặt lại | Cùng một hồ sơ xuyên suốt các phiên và thiết bị | | **Lưu trữ dữ liệu** | Dữ liệu của người dùng ẩn danh gắn với lần cài đặt ứng dụng | Dữ liệu của người dùng đã xác định được lưu trữ xuyên suốt các lần cài đặt ứng dụng | ## Người dùng ẩn danh \{#anonymous-users\} Nếu bạn không có xác thực backend, **bạn không cần xử lý xác thực trong code ứng dụng**: 1. Khi SDK được kích hoạt lần đầu tiên khởi chạy ứng dụng, Adapty **tạo một hồ sơ người dùng mới cho người dùng**. 2. Khi người dùng mua bất cứ thứ gì trong ứng dụng, giao dịch mua này được **liên kết với hồ sơ Adapty và tài khoản cửa hàng của họ**. 3. Khi người dùng **cài đặt lại** ứng dụng hoặc cài đặt từ **thiết bị mới**, Adapty **tạo một hồ sơ ẩn danh mới khi kích hoạt**. 4. Nếu người dùng đã thực hiện giao dịch mua trước đó trong ứng dụng của bạn, theo mặc định, các giao dịch mua của họ sẽ được tự động đồng bộ từ App Store khi SDK kích hoạt. :::note Khôi phục từ backup hoạt động khác với cài đặt lại. Theo mặc định, khi người dùng khôi phục từ backup, SDK giữ nguyên dữ liệu đã lưu trong bộ nhớ cache và không tạo hồ sơ mới. Bạn có thể cấu hình hành vi này bằng cài đặt `clearDataOnBackup`. [Tìm hiểu thêm](sdk-installation-ios#clear-data-on-backup-restore). ::: Vì vậy, với người dùng ẩn danh, hồ sơ mới sẽ được tạo mỗi lần cài đặt, nhưng đó không phải là vấn đề vì trong phân tích Adapty, bạn có thể [cấu hình những gì sẽ được coi là lần cài đặt mới](general#4-installs-definition-for-analytics). Đối với người dùng ẩn danh, bạn cần đếm lần cài đặt theo **ID thiết bị**. Trong trường hợp này, mỗi lần cài đặt ứng dụng trên một thiết bị được tính là một lần cài đặt, kể cả cài đặt lại. ## Người dùng đã xác định \{#identified-users\} Bạn có hai tùy chọn để xác định người dùng trong ứng dụng: - [**Trong quá trình đăng nhập/đăng ký:**](#during-loginsignup) Nếu người dùng đăng nhập sau khi ứng dụng khởi động, gọi `identify()` với customer user ID khi họ xác thực. - [**Trong quá trình kích hoạt SDK:**](#during-the-sdk-activation) Nếu bạn đã có customer user ID được lưu trữ khi ứng dụng khởi động, hãy gửi nó khi gọi `activate()`. :::important Theo mặc định, khi Adapty nhận được giao dịch mua từ một Customer User ID hiện đang được liên kết với Customer User ID khác, mức độ truy cập sẽ được chia sẻ, vì vậy cả hai hồ sơ đều có quyền truy cập trả phí. Bạn có thể cấu hình cài đặt này để chuyển quyền truy cập trả phí từ hồ sơ này sang hồ sơ khác hoặc tắt hoàn toàn việc chia sẻ. Xem [bài viết](general#6-sharing-paid-access-between-user-accounts) để biết thêm chi tiết. ::: ### Trong quá trình đăng nhập/đăng ký \{#during-loginsignup\} Nếu bạn xác định người dùng sau khi ứng dụng khởi động (ví dụ: sau khi họ đăng nhập vào ứng dụng hoặc đăng ký), hãy dùng phương thức `identify` để đặt customer user ID của họ. - Nếu bạn **chưa sử dụng customer user ID này trước đây**, Adapty sẽ tự động liên kết nó với hồ sơ hiện tại. - Nếu bạn **đã sử dụng customer user ID này để xác định người dùng trước đây**, Adapty sẽ chuyển sang làm việc với hồ sơ được liên kết với customer user ID này. :::important Customer user ID phải là duy nhất cho mỗi người dùng. Nếu bạn hardcode giá trị tham số, tất cả người dùng sẽ được coi là một. ::: Luôn `await` `identify` trước khi gọi các phương thức SDK khác. Các lệnh gọi đồng thời sẽ tạo ra lỗi `#3006 profileWasChanged` hoặc sẽ hoạt động trên hồ sơ ẩn danh. Xem [Thứ tự gọi trong iOS SDK](ios-sdk-call-order). ```swift showLineNumbers do { try await Adapty.identify("YOUR_USER_ID") // Unique for each user } catch { // handle the error } ``` ```swift showLineNumbers // User IDs must be unique for each user Adapty.identify("YOUR_USER_ID") { error in if let error { // handle the error } } ``` ### Trong quá trình kích hoạt SDK \{#during-the-sdk-activation\} Nếu bạn đã biết customer user ID khi kích hoạt SDK, bạn có thể gửi nó trong phương thức `activate` thay vì gọi `identify` riêng. Nếu bạn biết customer user ID nhưng chỉ đặt nó sau khi kích hoạt, điều đó có nghĩa là khi kích hoạt, Adapty sẽ tạo một hồ sơ ẩn danh mới và chỉ chuyển sang hồ sơ hiện có sau khi bạn gọi `identify`. Bạn có thể truyền customer user ID hiện có (đã dùng trước đó) hoặc một customer user ID mới. Nếu bạn truyền một customer user ID mới, hồ sơ ẩn danh được tạo khi kích hoạt sẽ tự động được liên kết với customer user ID đó. :::note Theo mặc định, việc tạo hồ sơ ẩn danh không ảnh hưởng đến dashboard phân tích, vì lần cài đặt được đếm dựa trên ID thiết bị. ID thiết bị đại diện cho một lần cài đặt ứng dụng từ cửa hàng trên một thiết bị và chỉ được tạo lại sau khi ứng dụng được cài đặt lại. Nó không phụ thuộc vào việc đây là lần cài đặt đầu tiên hay lần cài đặt lặp lại, hoặc liệu có sử dụng customer user ID hiện có hay không. Việc tạo hồ sơ (khi kích hoạt SDK hoặc đăng xuất), đăng nhập, hoặc nâng cấp ứng dụng mà không cài đặt lại không tạo ra các sự kiện cài đặt bổ sung. Nếu bạn muốn đếm lần cài đặt dựa trên người dùng duy nhất thay vì thiết bị, hãy vào **App settings** và cấu hình [**Installs definition for analytics**](general#4-installs-definition-for-analytics). ::: ```swift showLineNumbers // Place in the app main struct for SwiftUI or in AppDelegate for UIKit let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(customerUserId: "YOUR_USER_ID") // Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one. do { try await Adapty.activate(with: configurationBuilder.build()) } catch { // handle the error } ``` ```swift showLineNumbers // Place in the app main struct for SwiftUI or in AppDelegate for UIKit let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(customerUserId: "YOUR_USER_ID") // Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one. Adapty.activate(with: configurationBuilder.build()) { error in // handle the error } ``` ### Đăng xuất người dùng \{#log-users-out\} Nếu bạn có nút để đăng xuất người dùng, hãy sử dụng phương thức `logout`. :::important Đăng xuất người dùng sẽ tạo một hồ sơ ẩn danh mới cho người dùng. ::: ```swift showLineNumbers do { try await Adapty.logout() } catch { // handle the error } ``` ```swift showLineNumbers Adapty.logout { error in if error == nil { // successful logout } } ``` :::info Để đăng nhập lại người dùng vào ứng dụng, hãy sử dụng phương thức `identify`. ::: ### Cho phép mua hàng mà không cần đăng nhập \{#allow-purchases-without-login\} Nếu người dùng của bạn có thể thực hiện giao dịch mua cả trước và sau khi đăng nhập vào ứng dụng, bạn cần đảm bảo rằng họ vẫn giữ được quyền truy cập sau khi đăng nhập: 1. Khi người dùng chưa đăng nhập thực hiện giao dịch mua, Adapty liên kết nó với ID hồ sơ ẩn danh của họ. 2. Khi người dùng đăng nhập vào tài khoản của họ, Adapty chuyển sang làm việc với hồ sơ đã xác định của họ. - Nếu đây là customer user ID mới (ví dụ: giao dịch mua đã được thực hiện trước khi đăng ký), Adapty sẽ gán customer user ID cho hồ sơ hiện tại, do đó toàn bộ lịch sử mua hàng được duy trì. - Nếu đây là customer user ID hiện có (customer user ID đã được liên kết với một hồ sơ), bạn cần lấy mức độ truy cập thực tế sau khi chuyển đổi hồ sơ. Bạn có thể gọi [`getProfile`](ios-check-subscription-status) ngay sau khi xác định, hoặc [lắng nghe các cập nhật hồ sơ](ios-check-subscription-status) để dữ liệu tự động đồng bộ. ## Các bước tiếp theo \{#next-steps\} Chúc mừng! Bạn đã triển khai logic thanh toán trong ứng dụng! Chúc bạn thành công với việc kiếm tiền từ ứng dụng! Để khai thác thêm từ Adapty, bạn có thể khám phá các chủ đề sau: - [**Kiểm thử**](test-purchases-in-sandbox): Đảm bảo mọi thứ hoạt động như mong đợi - [**Onboardings**](ios-onboardings): Thu hút người dùng với onboarding và thúc đẩy tỷ lệ giữ chân - [**Tích hợp**](configuration): Tích hợp với các dịch vụ attribution marketing và phân tích chỉ trong một dòng code - [**Đặt thuộc tính hồ sơ tùy chỉnh**](setting-user-attributes): Thêm thuộc tính tùy chỉnh vào hồ sơ người dùng và tạo phân khúc, để bạn có thể chạy A/B test hoặc hiển thị các paywall khác nhau cho các người dùng khác nhau --- # File: adapty-sdk-integration-skill --- --- title: "Tích hợp Adapty vào ứng dụng iOS của bạn với kỹ năng tích hợp SDK" description: "Sử dụng kỹ năng adapty-sdk-integration để tích hợp Adapty SDK vào ứng dụng iOS của bạn từ đầu đến cuối với công cụ lập trình AI của bạn." --- :::important Kỹ năng này đang ở giai đoạn beta. Nếu nó bị treo hoặc hoạt động không như mong đợi, hãy làm theo [hướng dẫn tích hợp từng bước](adapty-cursor) — hướng dẫn này sẽ dẫn công cụ AI của bạn qua từng giai đoạn với tài liệu phù hợp. ::: --- no_index: true --- [Skill adapty-sdk-integration](https://github.com/adaptyteam/adapty-sdk-integration-skill) tự động hóa toàn bộ quá trình tích hợp Adapty: thiết lập dashboard, cài đặt SDK, paywall và xác minh từng giai đoạn. Skill tự động nhận diện nền tảng của bạn và tải tài liệu Adapty phù hợp ở mỗi giai đoạn. **Công cụ được hỗ trợ**: Claude Code, GitHub Copilot CLI, OpenAI Codex, Gemini CLI. Để cài đặt, chọn lệnh phù hợp với công cụ của bạn. Danh sách đầy đủ có trong [README của skill](https://github.com/adaptyteam/adapty-sdk-integration-skill). **Claude Code** ``` claude plugin marketplace add adaptyteam/adapty-sdk-integration-skill claude plugin install adapty-sdk-integration@adapty ``` **GitHub Copilot CLI** ``` gh skill install adaptyteam/adapty-sdk-integration-skill ``` **Gemini CLI** ``` gemini skills install https://github.com/adaptyteam/adapty-sdk-integration-skill ``` **OpenAI Codex hoặc bất kỳ công cụ nào khác**: Clone repo và sao chép thư mục `plugins/adapty-sdk-integration/skills/adapty-sdk-integration/` vào thư mục skills của công cụ bạn đang dùng. Sau khi cài đặt, chạy skill trong dự án của bạn: ``` /adapty-sdk-integration ``` Skill sẽ hỏi một vài câu hỏi thiết lập, sau đó hướng dẫn bạn qua các bước: thiết lập dashboard, cài đặt SDK, paywall và xác minh. --- # File: adapty-cursor --- --- title: "Tích hợp Adapty vào ứng dụng iOS với sự hỗ trợ của AI" description: "Hướng dẫn từng bước tích hợp Adapty vào ứng dụng iOS sử dụng Cursor, Context7, ChatGPT, Claude hoặc các công cụ AI khác." --- Hướng dẫn này sẽ đưa bạn qua từng bước tích hợp Adapty vào ứng dụng iOS với sự hỗ trợ của công cụ lập trình AI — bạn cung cấp đúng tài liệu Adapty theo đúng thứ tự. For a fully automated integration, use the [adapty-sdk-integration skill](https://github.com/adaptyteam/adapty-sdk-integration-skill): it runs the whole integration from your AI coding tool in one command. ## Trước khi bắt đầu: thiết lập dashboard \{#before-you-start-dashboard-setup\} Adapty yêu cầu một số cấu hình trên dashboard trước khi bạn viết bất kỳ code SDK nào. Bạn có thể thực hiện điều này với kỹ năng LLM tương tác, hoặc thủ công thông qua Dashboard. ### Cách tiếp cận với Skill (khuyến nghị) \{#skill-approach-recommended\} Kỹ năng Adapty CLI cho phép LLM của bạn thiết lập ứng dụng, sản phẩm, mức độ truy cập, paywall và placement trực tiếp — mà không cần mở Dashboard cho từng bước. Bạn chỉ cần [kết nối cửa hàng của mình](integrate-payments) trong Dashboard. ``` npx skills add adaptyteam/adapty-cli --skill adapty-cli ``` Sau khi thêm kỹ năng, chạy `/adapty-cli` trong agent của bạn. Nó sẽ hướng dẫn bạn từng bước — bao gồm cả khi nào cần mở Dashboard để kết nối cửa hàng. ### Cách tiếp cận thủ công qua Dashboard \{#dashboard-approach\} Nếu bạn muốn cấu hình mọi thứ thủ công, đây là những gì bạn cần trước khi viết code. LLM của bạn không thể tra cứu các giá trị trên dashboard thay cho bạn — bạn cần tự cung cấp chúng. 1. **Kết nối app store của bạn**: Trong Adapty Dashboard, vào **App settings → General**. Đây là yêu cầu bắt buộc để mua hàng hoạt động. [Kết nối App Store](integrate-payments) 2. **Sao chép Public SDK key của bạn**: Trong Adapty Dashboard, vào **App settings → General**, sau đó tìm phần **API keys**. Trong code, đây là chuỗi bạn truyền vào `Adapty.activate("YOUR_PUBLIC_SDK_KEY")`. 3. **Tạo ít nhất một sản phẩm**: Trong Adapty Dashboard, vào trang **Products**. Bạn không tham chiếu trực tiếp đến sản phẩm trong code — Adapty phân phối chúng thông qua flow hoặc paywall. [Thêm sản phẩm](quickstart-products) 4. **Tạo một flow hoặc paywall và một placement**: Trong Adapty Dashboard, tạo một flow (hoặc paywall nếu bạn tự xây dựng giao diện), sau đó gán nó cho một placement trên trang **Placements**. Trong code, placement ID là chuỗi bạn truyền vào `Adapty.getFlow("YOUR_PLACEMENT_ID")`. [Tạo flow](quickstart-paywalls) 5. **Thiết lập mức độ truy cập**: Trong Adapty Dashboard, cấu hình cho từng sản phẩm trên trang **Products**. Trong code, chuỗi được kiểm tra trong `profile.accessLevels["premium"]`. Mức độ truy cập `premium` mặc định phù hợp với hầu hết ứng dụng. Nếu người dùng trả phí được truy cập vào các tính năng khác nhau tùy theo sản phẩm (ví dụ, gói `basic` so với gói `pro`), hãy [tạo các mức độ truy cập bổ sung](assigning-access-level-to-a-product) trước khi bắt đầu lập trình. :::tip Khi bạn có đủ năm mục này, bạn đã sẵn sàng viết code. Hãy nói với LLM của bạn: "Public SDK key của tôi là X, placement ID của tôi là Y" để nó có thể tạo code khởi tạo và lấy paywall chính xác. ::: ### Thiết lập khi sẵn sàng \{#set-up-when-ready\} Những mục này không bắt buộc để bắt đầu lập trình, nhưng bạn sẽ cần chúng khi tích hợp trưởng thành hơn: - **A/B test**: Cấu hình trên trang **Placements**. Không cần thay đổi code. [A/B test](ab-tests) - **Thêm flow và placement**: Thêm nhiều lệnh gọi `getFlow` với các placement ID khác nhau. - **Tích hợp analytics**: Cấu hình trên trang **Integrations**. Thiết lập khác nhau tùy theo tích hợp. Xem [tích hợp analytics](analytics-integration) và [tích hợp attribution](attribution-integration). ## Cung cấp tài liệu Adapty cho LLM của bạn \{#feed-adapty-docs-to-your-llm\} ### Sử dụng Context7 (khuyến nghị) \{#use-context7-recommended\} [Context7](https://context7.com) là một máy chủ MCP cung cấp cho LLM của bạn quyền truy cập trực tiếp vào tài liệu Adapty cập nhật. LLM của bạn tự động lấy đúng tài liệu dựa trên những gì bạn hỏi — không cần dán URL thủ công. Context7 hoạt động với **Cursor**, **Claude Code**, **Windsurf** và các công cụ tương thích MCP khác. Để thiết lập, chạy: ``` npx ctx7 setup ``` Lệnh này phát hiện trình soạn thảo của bạn và cấu hình máy chủ Context7. Để thiết lập thủ công, xem [kho lưu trữ GitHub của Context7](https://github.com/upstash/context7). Sau khi cấu hình, hãy tham chiếu thư viện Adapty trong các prompt của bạn: ``` Use the adaptyteam/adapty-docs library to look up how to install the iOS SDK ``` :::warning Dù Context7 giúp bạn không cần dán link tài liệu thủ công, thứ tự triển khai vẫn quan trọng. Hãy làm theo [hướng dẫn triển khai](#implementation-walkthrough) bên dưới từng bước để đảm bảo mọi thứ hoạt động đúng. ::: ### Sử dụng tài liệu dạng văn bản thuần \{#use-plain-text-docs\} Bạn có thể truy cập bất kỳ tài liệu Adapty nào dưới dạng văn bản thuần Markdown. Thêm `.md` vào cuối URL của nó, hoặc nhấp vào **Copy for LLM** bên dưới tiêu đề bài viết. Ví dụ: [adapty-cursor.md](https://adapty.io/docs/vi/adapty-cursor.md). Mỗi giai đoạn trong [hướng dẫn triển khai](#implementation-walkthrough) bên dưới bao gồm một khối "Gửi đến LLM của bạn" với các link `.md` để dán. Để có thêm tài liệu cùng một lúc, xem [tệp chỉ mục và tập hợp con theo nền tảng](#plain-text-doc-index-files) bên dưới. ## Hướng dẫn triển khai \{#implementation-walkthrough\} Phần còn lại của hướng dẫn này đi qua việc tích hợp Adapty theo thứ tự triển khai. Mỗi giai đoạn bao gồm các tài liệu cần gửi cho LLM, những gì bạn sẽ thấy khi hoàn thành và các vấn đề thường gặp. ### Lên kế hoạch tích hợp \{#plan-your-integration\} Trước khi bắt đầu viết code, hãy yêu cầu LLM phân tích dự án của bạn và tạo kế hoạch triển khai. Nếu công cụ AI của bạn hỗ trợ chế độ lập kế hoạch (như chế độ plan của Cursor hoặc Claude Code), hãy sử dụng nó để LLM có thể đọc cả cấu trúc dự án lẫn tài liệu Adapty trước khi viết code. Hãy cho LLM biết bạn sử dụng cách tiếp cận nào cho việc mua hàng — điều này ảnh hưởng đến các hướng dẫn nó nên tuân theo: - [**Adapty Flow Builder**](adapty-flow-builder): Bạn tạo flow trong trình xây dựng no-code của Adapty, và SDK hiển thị chúng tự động. - [**Paywall tự tạo thủ công**](ios-quickstart-manual): Bạn tự xây dựng giao diện paywall trong code nhưng vẫn dùng Adapty để lấy sản phẩm và xử lý mua hàng. - [**Observer mode**](observer-vs-full-mode): Bạn giữ nguyên cơ sở hạ tầng mua hàng hiện có và chỉ dùng Adapty cho analytics và tích hợp. Chưa biết chọn cái nào? Đọc [bảng so sánh trong quickstart](ios-quickstart-paywalls). ### Cài đặt và cấu hình SDK \{#install-and-configure-the-sdk\} Cài đặt gói Adapty SDK qua Swift Package Manager trong Xcode và kích hoạt nó với Public SDK key của bạn. Đây là nền tảng — không có gì hoạt động được nếu thiếu bước này. **Hướng dẫn:** [Cài đặt & cấu hình Adapty SDK](sdk-installation-ios) Gửi đến LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/sdk-installation-ios.md ``` :::tip[Điểm kiểm tra] - **Kết quả mong đợi:** Ứng dụng build và chạy được. Console Xcode hiển thị log kích hoạt Adapty. - **Lưu ý:** "Public API key is missing" → kiểm tra xem bạn đã thay placeholder bằng key thực từ App settings chưa. ::: ### Hiển thị flow hoặc paywall và xử lý mua hàng \{#show-flows-or-paywalls-and-handle-purchases\} Lấy một flow hoặc paywall theo placement ID, hiển thị nó và xử lý các sự kiện mua hàng. Các hướng dẫn bạn cần phụ thuộc vào cách bạn xử lý mua hàng. Hãy kiểm tra từng giao dịch trong sandbox khi bạn tiến hành — đừng đợi đến cuối. Xem [Kiểm tra mua hàng trong sandbox](test-purchases-in-sandbox) để biết hướng dẫn thiết lập. **Hướng dẫn:** - [Kích hoạt mua hàng với Flow Builder (quickstart)](ios-quickstart-paywalls) - [Lấy flow và cấu hình của chúng](get-pb-paywalls) - [Hiển thị flow](ios-present-paywalls) - [Xử lý sự kiện flow](ios-handling-events) - [Phản hồi hành động nút](handle-paywall-actions) Gửi đến LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/ios-quickstart-paywalls.md - https://adapty.io/docs/vi/get-pb-paywalls.md - https://adapty.io/docs/vi/ios-present-paywalls.md - https://adapty.io/docs/vi/ios-handling-events.md - https://adapty.io/docs/vi/handle-paywall-actions.md ``` :::tip[Điểm kiểm tra] - **Kết quả mong đợi:** Flow hiển thị với các sản phẩm bạn đã cấu hình. Nhấn vào một sản phẩm sẽ kích hoạt hộp thoại mua hàng sandbox. - **Lưu ý:** Flow trống hoặc lỗi `getFlow` → xác minh placement ID khớp chính xác với dashboard và placement đã được gán đối tượng. ::: **Hướng dẫn:** - [Kích hoạt mua hàng trong paywall tùy chỉnh của bạn (quickstart)](ios-quickstart-manual) - [Lấy paywall và sản phẩm](fetch-paywalls-and-products) - [Hiển thị paywall được thiết kế bằng remote config](present-remote-config-paywalls) - [Thực hiện mua hàng](making-purchases) - [Khôi phục mua hàng](restore-purchase) Gửi đến LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/ios-quickstart-manual.md - https://adapty.io/docs/vi/fetch-paywalls-and-products.md - https://adapty.io/docs/vi/present-remote-config-paywalls.md - https://adapty.io/docs/vi/making-purchases.md - https://adapty.io/docs/vi/restore-purchase.md ``` :::tip[Điểm kiểm tra] - **Kết quả mong đợi:** Paywall tùy chỉnh của bạn hiển thị các sản phẩm được lấy từ Adapty. Nhấn vào một sản phẩm sẽ kích hoạt hộp thoại mua hàng sandbox. - **Lưu ý:** Mảng sản phẩm rỗng → xác minh paywall đã được gán sản phẩm trong dashboard và placement đã có đối tượng. ::: **Hướng dẫn:** - [Tổng quan về Observer mode](observer-vs-full-mode) - [Triển khai Observer mode](implement-observer-mode) - [Báo cáo giao dịch trong Observer mode](report-transactions-observer-mode) Gửi đến LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/observer-vs-full-mode.md - https://adapty.io/docs/vi/implement-observer-mode.md - https://adapty.io/docs/vi/report-transactions-observer-mode.md ``` :::tip[Điểm kiểm tra] - **Kết quả mong đợi:** Sau khi mua hàng sandbox bằng flow mua hàng hiện có của bạn, giao dịch xuất hiện trong **Event Feed** trên Adapty dashboard. - **Lưu ý:** Không có sự kiện → xác minh bạn đang báo cáo giao dịch cho Adapty và App Store Server Notifications đã được cấu hình. ::: ### Kiểm tra trạng thái gói đăng ký \{#check-subscription-status\} Sau khi mua hàng, kiểm tra hồ sơ người dùng để xem mức độ truy cập đang hoạt động nhằm giới hạn nội dung premium. **Hướng dẫn:** [Kiểm tra trạng thái gói đăng ký](ios-check-subscription-status) Gửi đến LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/ios-check-subscription-status.md ``` :::tip[Điểm kiểm tra] - **Kết quả mong đợi:** Sau khi mua hàng sandbox, `profile.accessLevels["premium"]?.isActive` trả về `true`. - **Lưu ý:** `accessLevels` rỗng sau khi mua → kiểm tra xem sản phẩm đã được gán mức độ truy cập trong dashboard chưa. ::: ### Xác định người dùng \{#identify-users\} Liên kết tài khoản người dùng trong ứng dụng của bạn với hồ sơ Adapty để giao dịch mua hàng được duy trì trên nhiều thiết bị. :::important Bỏ qua bước này nếu ứng dụng của bạn không có xác thực. ::: **Hướng dẫn:** [Xác định người dùng](ios-quickstart-identify) Gửi đến LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/ios-quickstart-identify.md ``` :::tip[Điểm kiểm tra] - **Kết quả mong đợi:** Sau khi gọi `Adapty.identify("your-user-id")`, phần **Profiles** trên dashboard hiển thị ID người dùng tùy chỉnh của bạn. - **Lưu ý:** Gọi `identify` sau khi kích hoạt nhưng trước khi lấy paywall để tránh attribution hồ sơ ẩn danh. ::: ### Chuẩn bị phát hành \{#prepare-for-release\} Khi tích hợp của bạn hoạt động trong sandbox, hãy xem qua danh sách kiểm tra phát hành để đảm bảo mọi thứ sẵn sàng cho production. **Hướng dẫn:** [Danh sách kiểm tra phát hành](release-checklist) Gửi đến LLM của bạn: ``` Read these Adapty docs before releasing: - https://adapty.io/docs/vi/release-checklist.md ``` :::tip[Điểm kiểm tra] - **Kết quả mong đợi:** Tất cả các mục trong danh sách đã được xác nhận: kết nối cửa hàng, thông báo máy chủ, flow mua hàng, kiểm tra mức độ truy cập và yêu cầu về quyền riêng tư. - **Lưu ý:** Thiếu App Store Server Notifications → cấu hình trong **App settings → iOS SDK** hoặc sự kiện sẽ không xuất hiện trong dashboard. ::: ## Tệp chỉ mục tài liệu văn bản thuần \{#plain-text-doc-index-files\} Nếu bạn cần cung cấp cho LLM bối cảnh rộng hơn ngoài các trang riêng lẻ, chúng tôi lưu trữ các tệp chỉ mục liệt kê hoặc kết hợp toàn bộ tài liệu Adapty: - [`llms.txt`](https://adapty.io/docs/vi/llms.txt): Liệt kê tất cả các trang với các link `.md`. Đây là [tiêu chuẩn đang nổi lên](https://llmstxt.org/) để làm cho các trang web có thể truy cập được bởi LLM. Lưu ý rằng đối với một số AI agent (ví dụ: ChatGPT), bạn sẽ cần tải xuống `llms.txt` và tải nó lên chat dưới dạng tệp. - [`llms-full.txt`](https://adapty.io/docs/vi/llms-full.txt): Toàn bộ tài liệu Adapty được kết hợp thành một tệp duy nhất. Rất lớn — chỉ sử dụng khi bạn cần toàn bộ nội dung. - Tập hợp con dành riêng cho iOS: [`ios-llms.txt`](https://adapty.io/docs/vi/ios-llms.txt) và [`ios-llms-full.txt`](https://adapty.io/docs/vi/ios-llms-full.txt): Tiết kiệm token hơn so với toàn bộ tài liệu. --- # File: get-pb-paywalls --- --- title: "Lấy flows & paywalls - iOS" description: "Tải flows và paywalls từ Adapty trong ứng dụng iOS của bạn." --- Sau khi [bạn đã thiết kế flow hoặc paywall trong Paywall Builder](adapty-paywall-builder), bạn có thể hiển thị nó trong ứng dụng di động của mình. Bước đầu tiên là lấy flow hoặc paywall được liên kết với placement và cấu hình view tương ứng như mô tả bên dưới. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. :::
Trước khi bắt đầu 1. [Tạo sản phẩm](create-product) của bạn trong Adapty Dashboard. 2. [Tạo flow/paywall và đưa sản phẩm vào đó](create-paywall) trong Adapty Dashboard. 3. [Tạo placement và đưa flow/paywall vào đó](create-placement) trong Adapty Dashboard. 4. Cài đặt [Adapty SDK](sdk-installation-ios) vào ứng dụng di động của bạn.
## Tải flow/paywall \{#fetch-flowpaywall\} Nếu bạn đã thiết kế một flow hoặc paywall bằng Flow Builder hoặc Paywall Builder, bạn không cần lo lắng về việc render nó trong code ứng dụng mobile để hiển thị cho người dùng. Flow hoặc paywall đó đã bao gồm cả nội dung cần hiển thị lẫn cách hiển thị. Tuy nhiên, bạn vẫn cần lấy ID của nó thông qua placement, cấu hình view, rồi mới trình bày nó trong ứng dụng mobile của bạn. Tải flow hoặc paywall và [cấu hình view](get-pb-paywalls#fetch-the-view-configuration) của nó càng sớm càng tốt — lý tưởng là trước khi hiển thị. Ngay khi bạn tải cấu hình view, SDK bắt đầu tải và cache hình ảnh trong nền. Tải càng sớm, các hình ảnh có càng nhiều thời gian để hoàn tất. Đến khi bạn hiển thị flow hoặc paywall, cấu hình và hình ảnh của nó đã có thể được cache sẵn sàng. Để lấy flow hoặc paywall, sử dụng phương thức `getFlow`: ```swift showLineNumbers do { let flow = try await Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID") // the requested flow/paywall } catch { // handle the error } ``` ```swift showLineNumbers Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID") { result in switch result { case let .success(flow): // the requested flow/paywall case let .failure(error): // handle the error } } ``` Tham số: | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` |

Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã lưu trong cache nếu thất bại. Chúng tôi khuyên dùng tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.

Tuy nhiên, nếu bạn cho rằng người dùng của mình thường gặp tình trạng kết nối internet không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu đã lưu trong cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng sẽ trải nghiệm tốc độ tải nhanh hơn dù kết nối có kém đến đâu. Cache được cập nhật định kỳ, vì vậy việc sử dụng nó trong phiên làm việc để tránh các yêu cầu mạng là hoàn toàn an toàn.

Lưu ý rằng cache vẫn được giữ nguyên sau khi khởi động lại ứng dụng và chỉ bị xóa khi cài đặt lại ứng dụng hoặc thực hiện dọn dẹp thủ công.

Adapty SDK lưu trữ paywall cục bộ theo hai lớp: cache được cập nhật định kỳ như mô tả ở trên và [paywall dự phòng](fallback-paywalls). Chúng tôi cũng sử dụng CDN để tải paywall nhanh hơn và một máy chủ dự phòng độc lập trong trường hợp CDN không thể truy cập. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản paywall mới nhất trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet yếu.

| | **loadTimeout** | mặc định: 5 giây |

Giá trị này giới hạn thời gian chờ cho phương thức này. Nếu hết thời gian chờ, dữ liệu đã lưu trong cache hoặc fallback cục bộ sẽ được trả về.

Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể timeout muộn hơn một chút so với giá trị được chỉ định trong `loadTimeout`, vì thao tác này có thể bao gồm nhiều yêu cầu khác nhau ở bên dưới.

| Tham số phản hồi: | Tham số | Mô tả | | :-------- | :---------- | | Flow | Một đối tượng `AdaptyFlow` chứa placement, các định danh (`id`, `variationId`), tên, Remote Config, và cờ `hasViewConfiguration` cho biết flow có bao gồm cấu hình giao diện hay không. Để lấy các sản phẩm thực tế phục vụ việc tải trước, giao diện tùy chỉnh, hoặc kiểm tra theo chương trình, hãy gọi `getPaywallProducts(flow:)`. | ## Lấy cấu hình giao diện \{#fetch-the-view-configuration\} Sau khi lấy flow hoặc paywall, hãy kiểm tra xem nó có bao gồm cấu hình giao diện hay không thông qua `flow.hasViewConfiguration`. Cờ này cho biết cách placement được thiết kế trong Adapty Dashboard: - **`true`** — placement được thiết kế trong **Flow Builder** (một flow) hoặc **Paywall Builder** (một paywall). Adapty sẽ tự động hiển thị giao diện cho bạn. Tiếp tục với các bước bên dưới để lấy cấu hình giao diện và [hiển thị flow hoặc paywall](ios-present-paywalls). - **`false`** — placement là một paywall tùy chỉnh không có giao diện Builder. Sử dụng phương thức `getFlowConfiguration` để tải cấu hình view. ```swift showLineNumbers guard flow.hasViewConfiguration else { // handle as remote config paywall return } let flowConfiguration = try await AdaptyUI.getFlowConfiguration(forFlow: flow) ``` Các tham số: | Tham số | Bắt buộc | Mô tả | | :----------------------- | :------------- | :---------- | | **forFlow** | bắt buộc | Một đối tượng `AdaptyFlow` lấy được qua `Adapty.getFlow`. | | **locale** |

tùy chọn

mặc định: `nil`

| Mã định danh của [bản địa hóa paywall](add-paywall-locale-in-adapty-paywall-builder). Nhập dưới dạng mã ngôn ngữ với một hoặc hai subtag phân cách bằng `-` (ví dụ: `en`, `pt-br`). Xem [Bản địa hóa và mã locale](localizations-and-locale-codes). | | **loadTimeout** | mặc định: 5 giây | Giá trị này giới hạn thời gian chờ cho phương thức này. Nếu hết thời gian chờ, dữ liệu đã cache hoặc fallback cục bộ sẽ được trả về. Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể timeout muộn hơn một chút so với giá trị được chỉ định trong `loadTimeout`, do thao tác có thể bao gồm nhiều request khác nhau bên dưới. | | **products** | tùy chọn | Cung cấp một mảng các đối tượng `AdaptyPaywallProduct` để tối ưu hóa thời điểm hiển thị sản phẩm trên màn hình. Nếu truyền `nil`, AdaptyUI sẽ tự động fetch các sản phẩm cần thiết. | | **systemRequestsHandler** | tùy chọn | Một đối tượng tuân theo `AdaptySystemRequestsHandler` để xử lý các yêu cầu quyền hệ thống và đánh giá được kích hoạt bởi các hành động trong flow. Chỉ cần thiết nếu flow của bạn có các hành động như vậy. | | **assetsResolver** | tùy chọn | Một dictionary `[String: AdaptyCustomAsset]` dùng để ghi đè hình ảnh và video trong flow/paywall. Xem [Tùy chỉnh assets](#customize-assets). | | **timerResolver** | tùy chọn | Một đối tượng tuân theo `AdaptyTimerResolver` cung cấp ngày kết thúc cho các bộ đếm thời gian do nhà phát triển định nghĩa. Xem [Thiết lập bộ đếm thời gian do nhà phát triển định nghĩa](#set-up-developer-defined-timers). | Sau khi tải xong, [hiển thị flow/paywall](ios-present-paywalls). ## Lấy flow hoặc paywall cho đối tượng mặc định để tải nhanh hơn \{#get-a-flow-or-paywall-for-a-default-audience-to-fetch-it-faster\} Thông thường, flow và paywall được tải gần như ngay lập tức, nên bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và placement, và người dùng của bạn có kết nối internet yếu, việc tải flow hoặc paywall có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị một flow hoặc paywall mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị gì cả. Để giải quyết vấn đề này, bạn có thể dùng phương thức `getFlowForDefaultAudience`, phương thức này lấy flow hoặc paywall của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy flow hoặc paywall bằng phương thức `getFlow`, như đã trình bày trong phần [Lấy thông tin Paywall](get-pb-paywalls#fetch-paywall-designed-with-paywall-builder) ở trên. :::warning Lý do chúng tôi khuyến nghị dùng `getFlow` Phương thức `getFlowForDefaultAudience` có một số hạn chế đáng kể: - **Vấn đề tương thích ngược tiềm ẩn**: Nếu bạn cần hiển thị các paywall khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế các paywall hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng với phiên bản hiện tại (cũ) có thể gặp sự cố với các paywall không được hiển thị. - **Mất khả năng targeting**: Tất cả người dùng sẽ thấy cùng một paywall được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất đi khả năng targeting cá nhân hóa (bao gồm dựa trên quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của riêng bạn). Nếu bạn chấp nhận những hạn chế này để đổi lấy tốc độ tải flow hoặc paywall nhanh hơn, hãy sử dụng phương thức `getFlowForDefaultAudience` như sau. Nếu không, hãy tiếp tục dùng `getFlow` đã được mô tả [ở trên](get-pb-paywalls#fetch-paywall-designed-with-paywall-builder). ::: ```swift showLineNumbers Adapty.getFlowForDefaultAudience(placementId: "YOUR_PLACEMENT_ID") { result in switch result { case let .success(flow): // the requested flow case let .failure(error): // handle the error } } ``` | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` |

Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã cache nếu thất bại. Chúng tôi khuyến nghị dùng tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.

Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp tình trạng mạng không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng thời gian tải sẽ nhanh hơn bất kể kết nối mạng có yếu đến đâu. Cache được cập nhật thường xuyên nên hoàn toàn an toàn khi dùng trong phiên làm việc để tránh các yêu cầu mạng không cần thiết.

Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc xóa thủ công.

| ## Tùy chỉnh assets \{#customize-assets\} Để tùy chỉnh hình ảnh và video trong paywall/flow của bạn, hãy triển khai custom assets. Hình ảnh hero và video có ID được định sẵn: `hero_image` và `hero_video`. Trong một custom asset bundle, bạn nhắm đến các phần tử này theo ID của chúng và tùy chỉnh hành vi của chúng. Đối với các hình ảnh và video khác, bạn cần [đặt ID tùy chỉnh](custom-media) trong Adapty dashboard. Ví dụ, bạn có thể: - Hiển thị hình ảnh hoặc video khác nhau cho một số người dùng. - Hiển thị hình ảnh xem trước cục bộ trong khi hình ảnh chính từ xa đang tải. - Hiển thị hình ảnh xem trước trước khi phát video. - Cung cấp độ phân giải pixel của video để trình phát có thể dành sẵn không gian bố cục (tỷ lệ khung hình = `width / height`) trước khi video tải. Truyền `nil` để bỏ qua phần này. Dưới đây là ví dụ về cách cung cấp các asset tùy chỉnh thông qua một dictionary đơn giản: ```swift showLineNumbers let customAssets: [String: AdaptyCustomAsset] = [ // Show a local image using a custom ID "custom_image": .image( .uiImage(value: UIImage(named: "image_name")!) ), // Show a local preview image while a remote main image is loading "hero_image": .image( .remote( url: URL(string: "https://example.com/image.jpg")!, preview: UIImage(named: "preview_image") ) ), // Show a local video with a preview image and a known resolution "hero_video": .video( .file( url: Bundle.main.url(forResource: "custom_video", withExtension: "mp4")!, preview: .uiImage(value: UIImage(named: "video_preview")!), resolution: CGSize(width: 1080, height: 1920) ) ), ] let flowConfig = try await AdaptyUI.getFlowConfiguration( forFlow: flow, assetsResolver: customAssets ) ``` :::note Nếu không tìm thấy asset, paywall/flow sẽ hiển thị theo giao diện mặc định. ::: ## Thiết lập timer do lập trình viên định nghĩa \{#set-up-developer-defined-timers\} Để sử dụng timer tùy chỉnh trong ứng dụng, hãy tạo một object tuân theo protocol `AdaptyTimerResolver`. Object này định nghĩa cách hiển thị từng timer tùy chỉnh. Nếu muốn, bạn có thể dùng trực tiếp dictionary `[String: Date]` vì nó đã tương thích với protocol này. Dưới đây là ví dụ: ```swift showLineNumbers @MainActor struct AdaptyTimerResolverImpl: AdaptyTimerResolver { func timerEndAtDate(for timerId: String) -> Date { switch timerId { case "CUSTOM_TIMER_6H": Date(timeIntervalSinceNow: 3600.0 * 6.0) // 6 hours case "CUSTOM_TIMER_NY": Calendar.current.date(from: DateComponents(year: 2025, month: 1, day: 1)) ?? Date(timeIntervalSinceNow: 3600.0) default: Date(timeIntervalSinceNow: 3600.0) // 1 hour } } } ``` Trong ví dụ này, `CUSTOM_TIMER_NY` và `CUSTOM_TIMER_6H` là các **Timer ID** của các timer do developer tự định nghĩa mà bạn đã thiết lập trong Adapty Dashboard. `timerResolver` đảm bảo ứng dụng của bạn cập nhật động từng timer với giá trị chính xác. Ví dụ: - `CUSTOM_TIMER_NY`: Thời gian còn lại cho đến khi timer kết thúc, chẳng hạn như Năm Mới. - `CUSTOM_TIMER_6H`: Thời gian còn lại trong khoảng 6 giờ bắt đầu từ khi người dùng mở paywall.
Sau khi [bạn đã thiết kế phần hiển thị cho paywall của mình](adapty-paywall-builder) bằng Paywall Builder trong Adapty Dashboard, bạn có thể hiển thị nó trong ứng dụng di động. Bước đầu tiên là lấy paywall liên kết với placement và cấu hình giao diện của nó như mô tả bên dưới. Lưu ý rằng chủ đề này đề cập đến các paywall được tùy chỉnh bằng Paywall Builder. Nếu bạn đang triển khai paywall theo cách thủ công, hãy tham khảo [Lấy paywall và sản phẩm cho paywall Remote Config](fetch-paywalls-and-products). :::tip Bạn muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, bao gồm toàn bộ thiết lập, từ hiển thị paywall, thực hiện mua hàng đến các chức năng cơ bản khác. :::
Trước khi bắt đầu hiển thị paywall trong ứng dụng di động của bạn 1. [Tạo sản phẩm của bạn](create-product) trong Adapty Dashboard. 2. [Tạo paywall và thêm sản phẩm vào đó](create-paywall) trong Adapty Dashboard. 3. [Tạo placement và thêm paywall vào đó](create-placement) trong Adapty Dashboard. 4. Cài đặt [Adapty SDK](sdk-installation-ios) vào ứng dụng di động của bạn.
## Tải paywall được thiết kế bằng Paywall Builder \{#fetch-paywall-designed-with-paywall-builder\} Nếu bạn đã [thiết kế paywall bằng Paywall Builder](adapty-paywall-builder), bạn không cần lo lắng về việc render nó trong code ứng dụng để hiển thị cho người dùng. Paywall như vậy đã bao gồm cả nội dung hiển thị lẫn cách thức hiển thị. Tuy nhiên, bạn vẫn cần lấy ID của nó thông qua placement, cấu hình view, rồi mới trình bày nó trong ứng dụng. Để đảm bảo hiệu suất tối ưu, hãy lấy paywall và [cấu hình hiển thị](get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) của nó càng sớm càng tốt, nhờ đó ảnh có đủ thời gian tải xuống trước khi hiển thị cho người dùng. Để lấy paywall, sử dụng phương thức `getPaywall`: ```swift showLineNumbers do { let paywall = try await Adapty.getPaywall("YOUR_PLACEMENT_ID") // the requested paywall } catch { // handle the error } ``` ```swift showLineNumbers Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en") { result in switch result { case let .success(paywall): // the requested paywall case let .failure(error): // handle the error } } ``` Tham số: | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** |

tùy chọn

mặc định: `en`

|

Mã định danh của [bản dịch paywall](add-paywall-locale-in-adapty-paywall-builder). Tham số này được kỳ vọng là mã ngôn ngữ gồm một hoặc hai thẻ con được phân cách bằng ký tự gạch ngang (**-**). Thẻ con đầu tiên là ngôn ngữ, thẻ con thứ hai là khu vực.

Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha (Brazil).

Xem [Bản dịch và mã locale](localizations-and-locale-codes) để biết thêm thông tin về mã locale và cách chúng tôi khuyến nghị sử dụng chúng.

| | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` |

Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã cache nếu thất bại. Chúng tôi khuyến nghị cách này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.

Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp tình trạng mạng không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng sẽ tải nhanh hơn bất kể kết nối mạng có chập chờn đến đâu. Cache được cập nhật thường xuyên, nên hoàn toàn an toàn khi dùng trong phiên làm việc để tránh các yêu cầu mạng.

Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc thực hiện dọn dẹp thủ công.

Adapty SDK lưu trữ paywall cục bộ theo hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và [paywall dự phòng](fallback-paywalls). Chúng tôi cũng sử dụng CDN để tải paywall nhanh hơn và một máy chủ dự phòng độc lập trong trường hợp CDN không thể truy cập. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản mới nhất của paywall, đồng thời đảm bảo độ tin cậy ngay cả khi kết nối mạng yếu.

| | **loadTimeout** | mặc định: 5 giây |

Giá trị này giới hạn thời gian chờ cho phương thức này. Nếu hết thời gian chờ, dữ liệu đã cache hoặc fallback cục bộ sẽ được trả về.

Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ trễ hơn một chút so với giá trị đã chỉ định trong `loadTimeout`, do thao tác này có thể bao gồm nhiều yêu cầu khác nhau bên dưới.

| Tham số phản hồi: | Tham số | Mô tả | | :-------- | :---------- | | Paywall | Đối tượng [`AdaptyPaywall`](https://swift.adapty.io/documentation/adapty/adaptypaywall) chứa danh sách ID sản phẩm, định danh paywall, Remote Config và một số thuộc tính khác. | ## Lấy cấu hình hiển thị của paywall được thiết kế bằng Paywall Builder \{#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder\} :::important Hãy đảm bảo bật toggle **Show on device** trong Paywall Builder. Nếu tùy chọn này chưa được bật, cấu hình hiển thị sẽ không thể lấy được. ::: Sau khi lấy paywall, hãy kiểm tra xem nó có chứa cấu hình hiển thị hay không — đây là dấu hiệu cho thấy paywall được tạo bằng Paywall Builder. Điều này sẽ giúp bạn biết cách hiển thị paywall. Nếu cấu hình hiển thị có mặt, hãy xử lý nó như một paywall Paywall Builder; nếu không, [xử lý nó như một paywall Remote Config](present-remote-config-paywalls). Sử dụng phương thức `getPaywallConfiguration` để tải cấu hình view. ```swift showLineNumbers guard paywall.hasViewConfiguration else { // use your custom logic return } do { let paywallConfiguration = try await AdaptyUI.getPaywallConfiguration( forPaywall: paywall, products: products ) // use loaded configuration } catch { // handle the error } ``` Tham số: | Tham số | Bắt buộc | Mô tả | | :----------------------- | :------------- | :---------- | | **paywall** | bắt buộc | Đối tượng `AdaptyPaywall` để lấy controller cho paywall mong muốn. | | **loadTimeout** | mặc định: 5 giây | Giá trị này giới hạn timeout cho phương thức này. Nếu hết thời gian chờ, dữ liệu được cache hoặc fallback cục bộ sẽ được trả về. Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể timeout muộn hơn một chút so với giá trị được chỉ định trong `loadTimeout`, vì thao tác có thể bao gồm nhiều request khác nhau bên dưới. | | **products** | tùy chọn | Cung cấp một mảng các đối tượng `AdaptyPaywallProduct` để tối ưu hóa thời điểm hiển thị sản phẩm trên màn hình. Nếu truyền vào `nil`, AdaptyUI sẽ tự động tải các sản phẩm cần thiết. | :::note Nếu bạn đang sử dụng nhiều ngôn ngữ, hãy tìm hiểu cách thêm [bản địa hóa Paywall Builder](add-paywall-locale-in-adapty-paywall-builder) và cách sử dụng mã locale đúng cách [tại đây](localizations-and-locale-codes). ::: Sau khi tải xong, hãy [hiển thị paywall](ios-present-paywalls). ## Lấy paywall cho đối tượng mặc định để tải nhanh hơn \{#get-a-paywall-for-a-default-audience-to-fetch-it-faster\} Thông thường, paywall được tải gần như ngay lập tức, nên bạn không cần lo lắng về việc tối ưu tốc độ. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và paywall, hoặc người dùng đang dùng kết nối internet yếu, việc tải paywall có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống đó, bạn có thể muốn hiển thị paywall mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị paywall nào cả. Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `getPaywallForDefaultAudience`, phương thức này sẽ lấy paywall của placement đã chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là phương pháp được khuyến nghị vẫn là lấy paywall qua phương thức `getPaywall`, như đã trình bày chi tiết trong phần [Lấy thông tin Paywall](get-pb-paywalls#fetch-paywall-designed-with-paywall-builder) ở trên. :::warning Lý do chúng tôi khuyến nghị sử dụng `getPaywall` Phương thức `getPaywallForDefaultAudience` có một số hạn chế đáng kể: - **Vấn đề tương thích ngược**: Nếu bạn cần hiển thị các paywall khác nhau cho các phiên bản ứng dụng khác nhau (phiên bản hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế các paywall hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng với phiên bản hiện tại (cũ) có thể gặp sự cố với các paywall không hiển thị được. - **Mất khả năng nhắm mục tiêu**: Tất cả người dùng sẽ thấy cùng một paywall được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất đi khả năng cá nhân hóa mục tiêu (bao gồm theo quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của riêng bạn). Nếu bạn sẵn sàng chấp nhận những hạn chế này để có được tốc độ tải paywall nhanh hơn, hãy sử dụng phương thức `getPaywallForDefaultAudience` như sau. Nếu không, hãy dùng `getPaywall` đã được mô tả [ở trên](get-pb-paywalls#fetch-paywall-designed-with-paywall-builder). ::: ```swift showLineNumbers Adapty.getPaywallForDefaultAudience(placementId: "YOUR_PLACEMENT_ID", locale: "en") { result in switch result { case let .success(paywall): // the requested paywall case let .failure(error): // handle the error } } ``` :::note Phương thức `getPaywallForDefaultAudience` khả dụng từ iOS SDK phiên bản 2.11.2 trở lên. ::: | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** |

tùy chọn

mặc định: `en`

|

Mã định danh của [bản dịch paywall](add-remote-config-locale). Tham số này là mã ngôn ngữ gồm một hoặc nhiều thẻ con phân cách bằng ký tự dấu trừ (**-**). Thẻ con đầu tiên là ngôn ngữ, thẻ con thứ hai là vùng.

Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha (Brazil).

Xem [Localizations và mã locale](localizations-and-locale-codes) để biết thêm thông tin về mã locale và cách chúng tôi khuyến nghị sử dụng.

| | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` |

Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã lưu trong bộ nhớ đệm nếu thất bại. Chúng tôi khuyến nghị lựa chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.

Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp tình trạng kết nối không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu từ bộ nhớ đệm nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng thời gian tải sẽ nhanh hơn dù kết nối internet kém đến đâu. Bộ nhớ đệm được cập nhật thường xuyên, nên hoàn toàn an toàn khi sử dụng trong phiên làm việc để tránh các yêu cầu mạng.

Lưu ý rằng bộ nhớ đệm vẫn tồn tại sau khi khởi động lại ứng dụng và chỉ bị xóa khi cài đặt lại ứng dụng hoặc xóa thủ công.

| ## Tùy chỉnh assets \{#customize-assets\} Để tùy chỉnh hình ảnh và video trong paywall của bạn, hãy triển khai custom assets. Hero image và video có ID được định sẵn: `hero_image` và `hero_video`. Trong một custom asset bundle, bạn nhắm tới các phần tử này bằng ID của chúng và tùy chỉnh hành vi của chúng. Đối với các hình ảnh và video khác, bạn cần [đặt ID tùy chỉnh](custom-media) trong Adapty dashboard. Ví dụ, bạn có thể: - Hiển thị hình ảnh hoặc video khác cho một số người dùng. - Hiển thị ảnh xem trước cục bộ trong khi hình ảnh chính từ xa đang tải. - Hiển thị ảnh xem trước trước khi phát video. :::important Để sử dụng tính năng này, hãy cập nhật Adapty iOS SDK lên phiên bản 3.7.0 trở lên. ::: Đây là ví dụ về cách bạn có thể cung cấp custom assets thông qua một dictionary đơn giản: ```swift showLineNumbers let customAssets: [String: AdaptyCustomAsset] = [ // Show a local image using a custom ID "custom_image": .image( .uiImage(value: UIImage(named: "image_name")!) ), // Show a local preview image while a remote main image is loading "hero_image": .image( .remote( url: URL(string: "https://example.com/image.jpg")!, preview: UIImage(named: "preview_image") ) ), // Show a local video with a preview image "hero_video": .video( .file( url: Bundle.main.url(forResource: "custom_video", withExtension: "mp4")!, preview: .uiImage(value: UIImage(named: "video_preview")!) ) ), ] let paywallConfig = try await AdaptyUI.getPaywallConfiguration( forPaywall: paywall, assetsResolver: customAssets ) ``` :::note Nếu không tìm thấy asset, paywall sẽ trở về giao diện mặc định. ::: ## Thiết lập timer do developer định nghĩa \{#set-up-developer-defined-timers\} Để sử dụng custom timer trong ứng dụng mobile, hãy tạo một object tuân theo protocol `AdaptyTimerResolver`. Object này xác định cách mỗi custom timer sẽ được hiển thị. Nếu muốn, bạn có thể dùng trực tiếp dictionary `[String: Date]`, vì nó đã tuân thủ protocol này. Dưới đây là ví dụ: ```swift showLineNumbers @MainActor struct AdaptyTimerResolverImpl: AdaptyTimerResolver { func timerEndAtDate(for timerId: String) -> Date { switch timerId { case "CUSTOM_TIMER_6H": Date(timeIntervalSinceNow: 3600.0 * 6.0) // 6 hours case "CUSTOM_TIMER_NY": Calendar.current.date(from: DateComponents(year: 2025, month: 1, day: 1)) ?? Date(timeIntervalSinceNow: 3600.0) default: Date(timeIntervalSinceNow: 3600.0) // 1 hour } } } ``` Trong ví dụ này, `CUSTOM_TIMER_NY` và `CUSTOM_TIMER_6H` là **Timer ID** của các timer do developer tự định nghĩa, được thiết lập trong Adapty Dashboard. `timerResolver` đảm bảo ứng dụng của bạn cập nhật động từng timer với giá trị chính xác. Ví dụ: - `CUSTOM_TIMER_NY`: Thời gian còn lại cho đến khi timer kết thúc, chẳng hạn như ngày Tết Dương lịch. - `CUSTOM_TIMER_6H`: Thời gian còn lại trong khoảng 6 giờ bắt đầu từ khi người dùng mở paywall.
--- # File: ios-present-paywalls --- --- title: "Hiển thị flows & paywalls - iOS" description: "Hiển thị flows và paywalls cho người dùng trong ứng dụng iOS của bạn." --- Nếu bạn đã tạo một flow hoặc paywall, bạn không cần lo lắng về việc render nó trong code ứng dụng để hiển thị cho người dùng. Flow hoặc paywall đó đã chứa cả nội dung cần hiển thị lẫn cách hiển thị. Để lấy đối tượng `AdaptyUI.FlowConfiguration` dùng bên dưới, xem [Lấy flows và paywalls](get-pb-paywalls). ## Hiển thị flows và paywalls trong SwiftUI \{#present-flows-and-paywalls-in-swiftui\} ### Hiển thị dạng modal view \{#present-as-a-modal-view\} Để hiển thị flow hoặc paywall trên màn hình thiết bị dưới dạng modal view, dùng modifier `.flow` trong SwiftUI. Cách gọi tối thiểu cần `isPresented`, `flowConfiguration`, và bốn callback bắt buộc: ```swift showLineNumbers title="SwiftUI" .flow( isPresented: $flowPresented, flowConfiguration: , didFailPurchase: { _, _ in /* handle the error */ }, didFinishRestore: { _ in /* check access level and dismiss */ }, didFailRestore: { _ in /* handle the error */ }, didReceiveError: { _ in flowPresented = false } ) ``` Để kiểm soát nhiều hơn, thêm các callback tùy chọn như `didPerformAction` để xử lý sự kiện nhấn nút và `didFinishPurchase` để phản hồi khi mua hàng thành công: ```swift showLineNumbers title="SwiftUI" @State var flowPresented = false // ensure that you manage this variable state and set it to `true` at the moment you want to show the flow or paywall var body: some View { Text("Hello, AdaptyUI!") .flow( isPresented: $flowPresented, flowConfiguration: , didPerformAction: { action in switch action { case .close: flowPresented = false default: // Handle other actions break } }, 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 } ) } ``` Các tham số: | Tham số | Bắt buộc | Mô tả | |:-----------------------|:---------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **isPresented** | bắt buộc | Một binding quản lý việc hiển thị màn hình flow hoặc paywall. | | **flowConfiguration** | bắt buộc | Đối tượng `AdaptyUI.FlowConfiguration` chứa thông tin hiển thị của flow hoặc paywall. Dùng phương thức `AdaptyUI.getFlowConfiguration(forFlow:)`. Xem [Lấy flows và paywalls](get-pb-paywalls) để biết thêm chi tiết. | | **didFailPurchase** | bắt buộc | Được gọi khi `Adapty.makePurchase()` thất bại. | | **didFinishRestore** | bắt buộc | Được gọi khi `Adapty.restorePurchases()` hoàn thành thành công. | | **didFailRestore** | bắt buộc | Được gọi khi `Adapty.restorePurchases()` thất bại. | | **didReceiveError** | bắt buộc | Được gọi khi có lỗi render hoặc lỗi runtime từ flow script (ví dụ: exception JavaScript, mã `AdaptyUIError` `4105`). Với lỗi render, hãy [liên hệ Adapty Support](mailto:support@adapty.io). | | **fullScreen** | tùy chọn | Xác định flow hoặc paywall hiển thị toàn màn hình hay dạng sheet. Mặc định là `true`. | | **didAppear** | tùy chọn | Được gọi khi view của flow hoặc paywall được hiển thị. | | **didDisappear** | tùy chọn | Được gọi khi view của flow hoặc paywall bị đóng. | | **didPerformAction** | tùy chọn | Được gọi khi người dùng nhấn một nút. Có hai action ID được định nghĩa sẵn: `close` và `openURL`; các ID còn lại là tùy chỉnh và có thể được thiết lập trong builder. | | **didSelectProduct** | tùy chọn | Được gọi khi người dùng hoặc hệ thống chọn một sản phẩm để mua. | | **didStartPurchase** | tùy chọn | Được gọi khi người dùng bắt đầu quá trình mua hàng. | | **didFinishPurchase** | tùy chọn | Được gọi khi `Adapty.makePurchase()` hoàn thành thành công. | | **didFinishWebPaymentNavigation** | tùy chọn | Được gọi khi điều hướng thanh toán web kết thúc. | | **didStartRestore** | tùy chọn | Được gọi khi người dùng bắt đầu quá trình khôi phục. | | **didFailLoadingProducts** | tùy chọn | Được gọi khi có lỗi trong quá trình tải sản phẩm. Trả về `true` để thử tải lại. | | **didPartiallyLoadProducts** | tùy chọn | Được gọi khi sản phẩm chỉ được tải một phần. | | **showAlertItem** | tùy chọn | Một binding quản lý việc hiển thị các alert item phía trên flow hoặc paywall. | | **showAlertBuilder** | tùy chọn | Hàm để render alert view. | | **placeholderBuilder** | tùy chọn | Hàm để render view placeholder trong khi flow hoặc paywall đang tải. Mặc định là `ProgressView`. | Xem chủ đề [iOS - Xử lý sự kiện](ios-handling-events) để biết thêm chi tiết về các tham số. ### Hiển thị dạng non-modal view \{#present-as-a-non-modal-view\} Bạn cũng có thể hiển thị flows và paywalls dưới dạng navigation destination hoặc inline view trong navigation flow của ứng dụng. Dùng `AdaptyFlowView` trực tiếp trong các SwiftUI view của bạn: ```swift showLineNumbers title="SwiftUI" AdaptyFlowView( flowConfiguration: , didFailPurchase: { product, error in // Handle purchase failure }, didFinishRestore: { profile in // Handle successful restore }, didFailRestore: { error in // Handle restore failure }, didReceiveError: { error in // Handle the error (rendering or JS exception from the flow script). } ) ``` ## Hiển thị flows và paywalls trong UIKit \{#present-flows-and-paywalls-in-uikit\} Để hiển thị flow hoặc paywall trên màn hình thiết bị, thực hiện các bước sau: 1. Khởi tạo visual flow bạn muốn hiển thị bằng phương thức `AdaptyUI.flowController(with:delegate:)`: ```swift showLineNumbers title="Swift" import AdaptyUI let visualFlow = try AdaptyUI.flowController( with: , delegate: ) ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :----------------------- | :------- | :---------- | | **flowConfiguration** | bắt buộc | Đối tượng `AdaptyUI.FlowConfiguration` chứa thông tin hiển thị của flow hoặc paywall. Dùng phương thức `AdaptyUI.getFlowConfiguration(forFlow:)`. Xem chủ đề [Lấy flows và paywalls](get-pb-paywalls) để biết thêm chi tiết. | | **delegate** | bắt buộc | Một `AdaptyFlowControllerDelegate` để lắng nghe các sự kiện của flow và paywall. Xem chủ đề [Xử lý sự kiện flow & paywall](ios-handling-events) để biết thêm chi tiết. | Giá trị trả về: | Đối tượng | Mô tả | | :---------------------- | :------------------------------------------------------- | | **AdaptyFlowController** | Đối tượng đại diện cho màn hình flow hoặc paywall được yêu cầu. | 2. Sau khi đối tượng được tạo thành công, bạn có thể hiển thị nó trên màn hình thiết bị: ```swift showLineNumbers title="Swift" present(visualFlow, animated: true) ``` :::tip Bạn muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywalls, thực hiện mua hàng và các chức năng cơ bản khác. ::: Nếu bạn đã tùy chỉnh paywall bằng Paywall Builder, bạn không cần lo lắng về việc render nó trong code ứng dụng để hiển thị cho người dùng. Paywall đó đã chứa cả nội dung cần hiển thị lẫn cách hiển thị. Để lấy đối tượng `AdaptyUI.PaywallConfiguration` dùng bên dưới, xem [Lấy paywalls Paywall Builder và cấu hình của chúng](get-pb-paywalls). ## Hiển thị paywalls trong SwiftUI \{#present-paywalls-in-swiftui\} ### Hiển thị dạng modal view \{#present-as-a-modal-view\} Để hiển thị visual paywall trên màn hình thiết bị dưới dạng modal view, dùng modifier `.paywall` trong SwiftUI: ```swift showLineNumbers title="SwiftUI" @State var paywallPresented = false // ensure that you manage this variable state and set it to `true` at the moment you want to show the paywall var body: some View { Text("Hello, AdaptyUI!") .paywall( isPresented: $paywallPresented, paywallConfiguration: , didPerformAction: { action in switch action { case .close: paywallPresented = false default: // Handle other actions break } }, didFinishPurchase: { product, profile in paywallPresented = false }, didFailPurchase: { product, error in /* handle the error */ }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, didFailRendering: { error in paywallPresented = false } ) } ``` Các tham số: | Tham số | Bắt buộc | Mô tả | |:----------------------------------|:---------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **isPresented** | bắt buộc | Một binding quản lý việc hiển thị màn hình paywall. | | **paywallConfiguration** | bắt buộc | Đối tượng `AdaptyUI.PaywallConfiguration` chứa thông tin hiển thị của paywall. Dùng phương thức `AdaptyUI.paywallConfiguration(for:products:viewConfiguration:observerModeResolver:tagResolver:timerResolver:)`. Xem chủ đề [Lấy paywalls Paywall Builder và cấu hình của chúng](get-pb-paywalls) để biết thêm chi tiết. | | **didFailPurchase** | bắt buộc | Được gọi khi `Adapty.makePurchase()` thất bại. | | **didFinishRestore** | bắt buộc | Được gọi khi `Adapty.restorePurchases()` hoàn thành thành công. | | **didFailRestore** | bắt buộc | Được gọi khi `Adapty.restorePurchases()` thất bại. | | **didFailRendering** | bắt buộc | Được gọi nếu có lỗi xảy ra khi render giao diện. Trong trường hợp này, hãy [liên hệ Adapty Support](mailto:support@adapty.io). | | **fullScreen** | tùy chọn | Xác định paywall hiển thị toàn màn hình hay dạng modal. Mặc định là `true`. | | **didAppear** | tùy chọn | Được gọi khi view của paywall được hiển thị. | | **didDisappear** | tùy chọn | Được gọi khi view của paywall bị đóng. | | **didPerformAction** | tùy chọn | Được gọi khi người dùng nhấn một nút. Các nút khác nhau có action ID khác nhau. Có hai action ID được định nghĩa sẵn: `close` và `openURL`; các ID còn lại là tùy chỉnh và có thể được thiết lập trong builder. | | **didSelectProduct** | tùy chọn | Được gọi khi người dùng hoặc hệ thống chọn một sản phẩm để mua. | | **didStartPurchase** | tùy chọn | Được gọi khi người dùng bắt đầu quá trình mua hàng. | | **didFinishPurchase** | tùy chọn | Được gọi khi `Adapty.makePurchase()` hoàn thành thành công. | | **didFinishWebPaymentNavigation** | tùy chọn | Được gọi khi điều hướng thanh toán web kết thúc. | | **didStartRestore** | tùy chọn | Được gọi khi người dùng bắt đầu quá trình khôi phục. | | **didFailLoadingProducts** | tùy chọn | Được gọi khi có lỗi trong quá trình tải sản phẩm. Trả về `true` để thử tải lại. | | **didPartiallyLoadProducts** | tùy chọn | Được gọi khi sản phẩm chỉ được tải một phần. | | **showAlertItem** | tùy chọn | Một binding quản lý việc hiển thị các alert item phía trên paywall. | | **showAlertBuilder** | tùy chọn | Hàm để render alert view. | | **placeholderBuilder** | tùy chọn | Hàm để render view placeholder trong khi paywall đang tải. | Xem chủ đề [iOS - Xử lý sự kiện](ios-handling-events) để biết thêm chi tiết về các tham số. ### Hiển thị dạng non-modal view \{#present-as-a-non-modal-view\} Bạn cũng có thể hiển thị paywalls dưới dạng navigation destination hoặc inline view trong navigation flow của ứng dụng. Dùng `AdaptyPaywallView` trực tiếp trong các SwiftUI view của bạn: ```swift showLineNumbers title="SwiftUI" AdaptyPaywallView( paywallConfiguration: , didFailPurchase: { product, error in // Handle purchase failure }, didFinishRestore: { profile in // Handle successful restore }, didFailRestore: { error in // Handle restore failure }, didFailRendering: { error in // Handle rendering error } ) ``` ## Hiển thị paywalls trong UIKit \{#present-paywalls-in-uikit\} Để hiển thị visual paywall trên màn hình thiết bị, thực hiện các bước sau: 1. Khởi tạo visual paywall bạn muốn hiển thị bằng phương thức `.paywallController(for:products:viewConfiguration:delegate:)`: ```swift showLineNumbers title="Swift" import AdaptyUI let visualPaywall = AdaptyUI.paywallController( with: , delegate: ) ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :----------------------- | :------- | :---------- | | **paywall configuration** | bắt buộc | Đối tượng `AdaptyUI.PaywallConfiguration` chứa thông tin hiển thị của paywall. Dùng phương thức `AdaptyUI.getPaywallConfiguration(forPaywall:locale:)`. Xem chủ đề [Lấy paywalls Paywall Builder và cấu hình của chúng](get-pb-paywalls) để biết thêm chi tiết. | | **delegate** | bắt buộc | Một `AdaptyPaywallControllerDelegate` để lắng nghe các sự kiện paywall. Xem chủ đề [Xử lý sự kiện paywall](ios-handling-events) để biết thêm chi tiết. | Giá trị trả về: | Đối tượng | Mô tả | | :---------------------- | :--------------------------------------------------- | | **AdaptyPaywallController** | Đối tượng đại diện cho màn hình paywall được yêu cầu | 2. Sau khi đối tượng được tạo thành công, bạn có thể hiển thị nó trên màn hình thiết bị: ```swift showLineNumbers title="Swift" present(visualPaywall, animated: true) ``` :::tip Bạn muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywalls, thực hiện mua hàng và các chức năng cơ bản khác. ::: --- # File: handle-paywall-actions --- --- title: "Phản hồi các hành động flow - iOS" description: "Xử lý các hành động nút bấm và xử lý đầu vào của người dùng từ các flow paywall và onboarding trong ứng dụng iOS của bạn." --- Nếu bạn đang xây dựng các flow hoặc paywall bằng Adapty Flow Builder hoặc Paywall Builder, việc thiết lập các nút bấm đúng cách là rất quan trọng: 1. Thêm [nút trong paywall builder](paywall-buttons) và gán cho nó một hành động có sẵn hoặc tạo một action ID tùy chỉnh. 2. Viết code trong ứng dụng để xử lý từng hành động bạn đã gán. Hướng dẫn này trình bày cách xử lý các hành động tùy chỉnh và hành động có sẵn trong code của bạn. :::warning **Chỉ việc đóng flow/paywall và mở URL được xử lý tự động.** Tất cả các hành động nút khác đều yêu cầu triển khai xử lý phù hợp trong code ứng dụng. ::: :::note iOS SDK có thể phản hồi các yêu cầu quyền hệ thống, chẳng hạn như thông báo đẩy hoặc quyền truy cập camera, thông qua `AdaptySystemRequestsHandler`. Flows chưa kích hoạt các yêu cầu này, vì vậy bạn chưa cần xử lý chúng. ::: ## Đóng flow và paywall \{#close-flows-and-paywalls\} Để thêm nút đóng flow hoặc paywall: 1. Trong builder, thêm một nút và gán cho nó hành động **Close**. 2. Trong code của ứng dụng, triển khai handler cho hành động `close`. :::info Trong iOS SDK, hành động `close` mặc định sẽ kích hoạt việc đóng flow hoặc paywall. Tuy nhiên, bạn có thể ghi đè hành vi này trong code nếu cần. Ví dụ: đóng một flow có thể kích hoạt mở một flow khác. ::: ```swift .flow( isPresented: $flowPresented, flowConfiguration: flowConfiguration, didPerformAction: { action in switch action { case .close: flowPresented = false // dismiss the flow or paywall default: break } }, 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 } ) ``` ## Mở URL từ flow và paywall \{#open-urls-from-flows-and-paywalls\} :::tip Nếu bạn muốn thêm một nhóm liên kết (ví dụ: điều khoản sử dụng và khôi phục mua hàng), hãy thêm phần tử **Link** trong builder và xử lý nó giống như các nút có hành động **Open URL**. ::: Để thêm nút mở liên kết từ flow hoặc paywall (ví dụ: **Terms of use** hoặc **Privacy policy**): 1. Trong builder, thêm một nút, gán cho nó hành động **Open URL**, và nhập URL bạn muốn mở. 2. Trong code ứng dụng, triển khai một handler cho hành động `openURL` để mở URL nhận được trong trình duyệt. :::info Trong iOS SDK, hành động `openURL` mặc định sẽ mở URL. Tuy nhiên, bạn có thể ghi đè hành vi này trong code nếu cần. ::: ```swift .flow( isPresented: $flowPresented, flowConfiguration: flowConfiguration, didPerformAction: { action in switch action { case let .openURL(url): UIApplication.shared.open(url, options: [:]) // default behavior default: break } }, 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 } ) ``` ## Xử lý các hành động tùy chỉnh \{#handle-custom-actions\} Để thêm một nút xử lý các hành động khác: 1. Trong builder, thêm một nút, gán cho nó hành động **Custom**, và gán cho nó một ID. 2. Trong code ứng dụng của bạn, implement một handler cho action ID bạn đã tạo. Ví dụ: nếu bạn có thêm một bộ ưu đãi gói đăng ký hoặc sản phẩm mua một lần khác, bạn có thể thêm một nút để hiển thị một flow hoặc paywall khác: ```swift .flow( isPresented: $flowPresented, flowConfiguration: flowConfiguration, didPerformAction: { action in switch action { case let .custom(id): if id == "openNewPaywall" { // Display another flow or paywall } default: break } }, 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 } ) ``` Nếu bạn đang xây dựng paywall bằng Adapty paywall builder, việc thiết lập các nút đúng cách là rất quan trọng: 1. Thêm [nút trong paywall builder](paywall-buttons) và gán cho nó một action có sẵn hoặc tạo một custom action ID. 2. Viết code trong ứng dụng để xử lý từng action bạn đã gán. Hướng dẫn này hướng dẫn cách xử lý các custom action và action có sẵn trong code của bạn. :::warning **Chỉ có mua hàng, khôi phục, đóng paywall và mở URL được xử lý tự động.** Tất cả các action nút khác đều yêu cầu triển khai xử lý phù hợp trong code ứng dụng. ::: ## Đóng paywall \{#close-paywalls\} Để thêm nút đóng paywall: 1. Trong Paywall Builder, thêm một nút và gán cho nó hành động **Close**. 2. Trong code của ứng dụng, triển khai handler cho hành động `close` để đóng paywall. :::info Trong iOS SDK, hành động `close` mặc định sẽ tự động đóng paywall. Tuy nhiên, bạn có thể ghi đè hành vi này trong code nếu cần. Ví dụ, đóng một paywall có thể kích hoạt mở paywall khác. ::: ```swift func paywallController(_ controller: AdaptyPaywallController, didPerform action: AdaptyUI.Action) { switch action { case .close: controller.dismiss(animated: true) // default behavior break } } ``` ## Mở URL từ paywall \{#open-urls-from-paywalls\} :::tip Nếu bạn muốn thêm một nhóm liên kết (ví dụ: điều khoản sử dụng và khôi phục giao dịch), hãy thêm phần tử **Link** trong paywall builder và xử lý nó theo cách tương tự như các nút có hành động **Open URL**. ::: Để thêm nút mở một liên kết từ paywall (ví dụ: **Terms of use** hoặc **Privacy policy**): 1. Trong paywall builder, thêm một nút, gán cho nó hành động **Open URL** và nhập URL bạn muốn mở. 2. Trong code của ứng dụng, triển khai handler cho hành động `openUrl` để mở URL nhận được trong trình duyệt. :::info Trong iOS SDK, hành động `openUrl` mặc định sẽ mở URL. Tuy nhiên, bạn có thể ghi đè hành vi này trong code nếu cần. ::: ```swift func paywallController(_ controller: AdaptyPaywallController, didPerform action: AdaptyUI.Action) { switch action { case let .openURL(url): UIApplication.shared.open(url, options: [:]) // default behavior break } } ``` ## Đăng nhập vào ứng dụng \{#log-into-the-app\} Để thêm nút đăng nhập cho người dùng vào ứng dụng của bạn: 1. Trong Paywall Builder, thêm một nút và gán cho nó hành động **Login**. 2. Trong code ứng dụng của bạn, triển khai một handler cho hành động `login` để xác định người dùng. ```swift func paywallController(_ controller: AdaptyPaywallController, didPerform action: AdaptyUI.Action) { switch action { case .login: // Show a login screen let loginVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "LoginViewController") controller.present(loginVC, animated: true) } } ``` ## Xử lý hành động tùy chỉnh \{#handle-custom-actions\} Để thêm nút xử lý các hành động khác: 1. Trong Paywall Builder, thêm một nút, gán cho nó hành động **Custom**, và gán cho nó một ID. 2. Trong code ứng dụng của bạn, implement handler cho ID hành động bạn đã tạo. Ví dụ: nếu bạn có một bộ ưu đãi gói đăng ký hoặc sản phẩm mua một lần khác, bạn có thể thêm nút để hiển thị một paywall khác: ```swift func paywallController(_ controller: AdaptyPaywallController, didPerform action: AdaptyUI.Action) { switch action { case let .custom(id): if id == "openNewPaywall" { // Display another paywall } } break } } ``` --- # File: ios-handling-events --- --- title: "Xử lý sự kiện flow & paywall - iOS" description: "Xử lý các sự kiện flow và paywall trong ứng dụng iOS của bạn." --- :::important Hướng dẫn này đề cập đến việc xử lý sự kiện cho các giao dịch mua, khôi phục, chọn sản phẩm và hiển thị paywall. Bạn cũng cần triển khai xử lý nút bấm (đóng paywall, mở liên kết, v.v.). Xem [hướng dẫn xử lý hành động nút bấm](handle-paywall-actions) của chúng tôi để biết thêm chi tiết. ::: Flow và paywall không cần thêm code để thực hiện hoặc khôi phục giao dịch mua. Tuy nhiên, chúng tạo ra một số sự kiện mà ứng dụng của bạn có thể phản hồi. Các sự kiện đó bao gồm thao tác nhấn nút (nút đóng, URL, lựa chọn sản phẩm, v.v.) cũng như thông báo về các hành động liên quan đến giao dịch mua. Tìm hiểu cách phản hồi các sự kiện này bên dưới. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, bao gồm đầy đủ thiết lập, từ hiển thị paywall, thực hiện giao dịch mua cho đến các chức năng cơ bản khác. ::: ## Xử lý sự kiện trong SwiftUI \{#handling-events-in-swiftui\} Để kiểm soát hoặc theo dõi các tiến trình xảy ra trên màn hình flow hoặc paywall trong ứng dụng di động của bạn, hãy sử dụng modifier `.flow` trong SwiftUI: ```swift showLineNumbers title="Swift" @State var flowPresented = false var body: some View { Text("Hello, AdaptyUI!") .flow( isPresented: $flowPresented, flowConfiguration: flowConfiguration, didPerformAction: { action in switch action { case .close: flowPresented = false case let .openURL(url): // handle opening the URL (incl. for terms and privacy) default: // handle other actions } }, didSelectProduct: { product in /* Handle the event */ }, didStartPurchase: { product in /* Handle the event */ }, didFailPurchase: { product, error in /* handle the error */ }, didStartRestore: { /* Handle the event */ }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, didReceiveError: { error in flowPresented = false }, didFailLoadingProducts: { error in // Return `true` to retry loading return false } ) } ``` Bạn chỉ cần đăng ký những tham số closure cần dùng và bỏ qua những tham số không cần thiết. | Tham số | Bắt buộc | Mô tả | |:-----------------------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **isPresented** | bắt buộc | Binding kiểm soát việc hiển thị màn hình flow hoặc paywall. | | **flowConfiguration** | bắt buộc | Đối tượng `AdaptyUI.FlowConfiguration` chứa thông tin hiển thị của flow hoặc paywall. Xem [Lấy flow và paywall](get-pb-paywalls) để biết thêm chi tiết. | | **didFailPurchase** | bắt buộc | Được gọi khi `Adapty.makePurchase()` thất bại. | | **didFinishRestore** | bắt buộc | Được gọi khi `Adapty.restorePurchases()` hoàn tất thành công. | | **didFailRestore** | bắt buộc | Được gọi khi `Adapty.restorePurchases()` thất bại. | | **didReceiveError** | bắt buộc | Được gọi khi flow gặp lỗi hiển thị hoặc lỗi runtime từ script của flow (ví dụ: ngoại lệ JavaScript, mã `AdaptyUIError` `4105`). Trong trường hợp lỗi hiển thị, hãy [liên hệ bộ phận hỗ trợ Adapty](mailto:support@adapty.io). | | **placeholderBuilder** | tùy chọn | Hàm dùng để hiển thị view placeholder trong khi flow hoặc paywall đang tải. Mặc định là `ProgressView`. | | **fullScreen** | tùy chọn | Xác định flow hoặc paywall hiển thị ở chế độ toàn màn hình hay dạng sheet. Mặc định là `true`. | | **didAppear** | tùy chọn | Được gọi khi view của flow hoặc paywall xuất hiện trên màn hình. | | **didDisappear** | tùy chọn | Được gọi khi view của flow hoặc paywall bị đóng lại. | | **didPerformAction** | tùy chọn | Được gọi khi người dùng nhấn một nút. Có hai action ID được định nghĩa sẵn: `close` và `openURL`; các ID còn lại là tùy chỉnh và có thể được thiết lập trong builder. | | **didSelectProduct** | tùy chọn | Được gọi khi người dùng hoặc hệ thống chọn một sản phẩm để mua. | | **didStartPurchase** | tùy chọn | Được gọi khi người dùng bắt đầu quá trình mua hàng. | | **didFinishPurchase** | tùy chọn | Được gọi khi `Adapty.makePurchase()` hoàn tất thành công. | | **didFinishWebPaymentNavigation** | tùy chọn | Được gọi khi điều hướng thanh toán web hoàn tất. | | **didStartRestore** | tùy chọn | Được gọi khi người dùng bắt đầu quá trình khôi phục. | | **didFailLoadingProducts** | tùy chọn | Được gọi khi xảy ra lỗi trong quá trình tải sản phẩm. Trả về `true` để thử tải lại. | | **didPartiallyLoadProducts** | tùy chọn | Được gọi khi sản phẩm được tải một phần. | | **showAlertItem** | tùy chọn | Binding kiểm soát việc hiển thị các mục cảnh báo phía trên flow hoặc paywall. | | **showAlertBuilder** | tùy chọn | Hàm dùng để hiển thị view cảnh báo. | ## Xử lý sự kiện trong UIKit \{#handling-events-in-uikit\} Đối với các ứng dụng UIKit, sự kiện được xử lý thông qua giao thức `AdaptyFlowControllerDelegate`. Xem [Hiển thị flow & paywall - iOS](ios-present-paywalls) để biết cách thiết lập `AdaptyFlowController` với `AdaptyFlowControllerDelegate`. Giao thức khai báo 13 phương thức. Ba trong số đó không có triển khai mặc định và bắt buộc phải được implement khi tuân thủ: `didFailPurchase`, `didFinishRestoreWith`, và `didFailRestoreWith`. Các phương thức còn lại cung cấp triển khai mặc định không làm gì (no-op) và có thể được ghi đè khi bạn muốn hành vi tùy chỉnh. Các phương thức được nhóm theo mục đích bên dưới. ### Vòng đời \{#lifecycle\} ```swift showLineNumbers title="Swift" func flowControllerDidAppear(_ controller: AdaptyFlowController) { } func flowControllerDidDisappear(_ controller: AdaptyFlowController) { } ``` Các hàm này được gọi khi flow hoặc màn hình paywall được hiển thị và bị đóng lại. ### Hành động của người dùng \{#user-actions\} ```swift showLineNumbers title="Swift" func flowController( _ controller: AdaptyFlowController, didPerform action: AdaptyUI.Action ) { } ``` Các trường hợp của `AdaptyUI.Action`: - `.close` — hành vi mặc định là đóng controller. Ghi đè nếu bạn muốn giữ controller trên màn hình hoặc thực hiện thêm thao tác dọn dẹp. - `.openURL(url:)` — hành vi mặc định là mở URL bằng `UIApplication.shared.open(...)`. - `.custom(id:)` — được kích hoạt cho các nút có ID hành động tùy chỉnh được thiết lập trong builder. ### Chọn sản phẩm \{#product-selection\} ```swift showLineNumbers title="Swift" func flowController( _ controller: AdaptyFlowController, didSelectProduct product: AdaptyPaywallProduct ) { } ``` Được gọi khi người dùng hoặc hệ thống chọn một sản phẩm để mua. Sản phẩm mang đầy đủ thông tin ưu đãi (tính đủ điều kiện được xác định tự động trong v4 — không còn kiểu `AdaptyPaywallProductWithoutDeterminingOffer` riêng biệt nữa). ### Sự kiện mua hàng \{#purchase-events\} ```swift showLineNumbers title="Swift" func flowController( _ controller: AdaptyFlowController, didStartPurchase product: AdaptyPaywallProduct ) { } func flowController( _ controller: AdaptyFlowController, didFinishPurchase product: AdaptyPaywallProduct, purchaseResult: AdaptyPurchaseResult ) { // Default: dismiss the controller unless the purchase was cancelled. } func flowController( _ controller: AdaptyFlowController, didFailPurchase product: AdaptyPaywallProduct, error: AdaptyError ) { } ``` `didFailPurchase` là sự kiện mua hàng duy nhất không có implementation mặc định. `didFinishPurchase` mặc định sẽ đóng controller khi mua thành công; chỉ cần override nếu bạn cần logic tùy chỉnh sau khi mua. ### Sự kiện khôi phục \{#restore-events\} ```swift showLineNumbers title="Swift" func flowControllerDidStartRestore(_ controller: AdaptyFlowController) { } func flowController( _ controller: AdaptyFlowController, didFinishRestoreWith profile: AdaptyProfile ) { } func flowController( _ controller: AdaptyFlowController, didFailRestoreWith error: AdaptyError ) { } ``` `didFinishRestoreWith` và `didFailRestoreWith` không có implementation mặc định. Hãy kiểm tra xem `AdaptyProfile` trả về có chứa mức độ truy cập mong muốn hay không trước khi dismiss controller. ### Lỗi flow và lỗi tải sản phẩm \{#flow-errors-and-product-loading-errors\} ```swift showLineNumbers title="Swift" func flowController( _ controller: AdaptyFlowController, didReceiveError error: AdaptyUIError ) { } func flowController( _ controller: AdaptyFlowController, didFailLoadingProductsWith error: AdaptyError ) -> Bool { // Return `true` to retry product loading; default returns `false`. return false } func flowController( _ controller: AdaptyFlowController, didPartiallyLoadProducts failedIds: [String] ) { } ``` `didReceiveError` kích hoạt khi có lỗi render và lỗi runtime từ script của flow (ngoại lệ JavaScript, mã `AdaptyUIError` `4105`). Với lỗi render, hãy [liên hệ Adapty Support](mailto:support@adapty.io). Với lỗi tải, trả về `true` từ `didFailLoadingProductsWith` để thử lại — hữu ích khi gặp sự cố mạng tạm thời. ### Điều hướng thanh toán web \{#web-payment-navigation\} ```swift showLineNumbers title="Swift" func flowController( _ controller: AdaptyFlowController, didFinishWebPaymentNavigation product: AdaptyPaywallProduct?, error: AdaptyError? ) { } ``` Được gọi sau khi điều hướng thanh toán web kết thúc, dù thành công hay thất bại. :::important Hướng dẫn này đề cập đến việc xử lý các sự kiện mua hàng, khôi phục, chọn sản phẩm và hiển thị paywall. Bạn cũng cần triển khai xử lý nút bấm (đóng paywall, mở liên kết, v.v.). Xem [hướng dẫn xử lý hành động nút bấm](handle-paywall-actions) để biết thêm chi tiết. ::: Paywall được cấu hình bằng [Paywall Builder](adapty-paywall-builder) không cần thêm code để thực hiện và khôi phục giao dịch mua. Tuy nhiên, chúng tạo ra một số sự kiện mà ứng dụng của bạn có thể phản hồi. Các sự kiện đó bao gồm thao tác nhấn nút (nút đóng, URL, chọn sản phẩm, v.v.) cũng như thông báo về các hành động liên quan đến giao dịch mua được thực hiện trên paywall. Hãy tham khảo bên dưới để biết cách phản hồi các sự kiện này. Hướng dẫn này chỉ dành cho **paywall Paywall Builder mới** yêu cầu Adapty SDK v3.0 trở lên. :::tip Bạn muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ## Xử lý sự kiện trong SwiftUI \{#handling-events-in-swiftui\} Để kiểm soát hoặc theo dõi các quá trình diễn ra trên màn hình paywall trong ứng dụng di động của bạn, hãy sử dụng modifier `.paywall` trong SwiftUI: ```swift showLineNumbers title="Swift" @State var paywallPresented = false var body: some View { Text("Hello, AdaptyUI!") .paywall( isPresented: $paywallPresented, paywall: paywall, viewConfiguration: viewConfig, didPerformAction: { action in switch action { case .close: paywallPresented = false case let .openURL(url): // handle opening the URL (incl. for terms and privacy) default: // handle other actions } }, didSelectProduct: { /* Handle the event */ }, didStartPurchase: { /* Handle the event */ }, didFinishPurchase: { product, info in /* Handle the event */ }, didFailPurchase: { product, error in /* Handle the event */ }, didStartRestore: { /* Handle the event */ }, didFinishRestore: { /* Handle the event */ }, didFailRestore: { /* Handle the event */ }, didFailRendering: { error in paywallPresented = false }, didFailLoadingProducts: { error in return false } ) } ``` Bạn chỉ cần đăng ký những tham số closure mà bạn cần, và bỏ qua những tham số không cần thiết. Trong trường hợp này, các tham số closure không được sử dụng sẽ không được tạo ra. | Tham số | Bắt buộc | Mô tả | |:----------------------------------|:---------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **isPresented** | bắt buộc | Một binding quản lý việc màn hình paywall có được hiển thị hay không. | | **paywallConfiguration** | bắt buộc | Một đối tượng `AdaptyUI.PaywallConfiguration` chứa các chi tiết hiển thị của paywall. Sử dụng phương thức `AdaptyUI.paywallConfiguration(for:products:viewConfiguration:observerModeResolver:tagResolver:timerResolver:)`. Tham khảo chủ đề [Fetch Paywall Builder paywalls and their configuration](get-pb-paywalls) để biết thêm chi tiết. | | **didFailPurchase** | bắt buộc | Được gọi khi giao dịch mua thất bại do lỗi (ví dụ: thanh toán không được phép, sự cố mạng, sản phẩm không hợp lệ). Không được gọi khi người dùng hủy hoặc thanh toán đang chờ xử lý. | | **didFinishRestore** | bắt buộc | Được gọi khi giao dịch mua hoàn tất thành công. | | **didFailRestore** | bắt buộc | Được gọi khi khôi phục giao dịch mua thất bại. | | **didFailRendering** | bắt buộc | Được gọi nếu xảy ra lỗi trong quá trình render giao diện. Trong trường hợp này, hãy [liên hệ Adapty Support](mailto:support@adapty.io). | | **fullScreen** | tùy chọn | Xác định paywall hiển thị ở chế độ toàn màn hình hay dạng modal. Mặc định là `true`. | | **didAppear** | tùy chọn | Được gọi khi view paywall xuất hiện trên màn hình. Cũng được gọi khi người dùng nhấn [nút web paywall](web-paywall#step-2a-add-a-web-purchase-button) bên trong một paywall và web paywall mở trong trình duyệt in-app. | | **didDisappear** | tùy chọn | Được gọi khi view paywall bị đóng. Cũng được gọi khi [web paywall](web-paywall#step-2a-add-a-web-purchase-button) được mở từ một paywall trong trình duyệt in-app biến mất khỏi màn hình. | | **didPerformAction** | tùy chọn | Được gọi khi người dùng nhấn một nút. Các nút khác nhau có ID hành động khác nhau. Hai ID hành động được định nghĩa sẵn là `close` và `openURL`, các ID còn lại là tùy chỉnh và có thể được thiết lập trong builder. | | **didSelectProduct** | tùy chọn | Được gọi khi một sản phẩm được chọn để mua (do người dùng hoặc hệ thống). | | **didStartPurchase** | tùy chọn | Được gọi khi người dùng bắt đầu quá trình mua hàng. | | **didFinishPurchase** | tùy chọn | Được gọi khi giao dịch mua hoàn tất thành công. | | **didFinishWebPaymentNavigation** | tùy chọn | Được gọi sau khi thử mở [web paywall](web-paywall) để mua hàng, dù thành công hay thất bại. | | **didStartRestore** | tùy chọn | Được gọi khi người dùng bắt đầu quá trình khôi phục. | | **didFailLoadingProducts** | tùy chọn | Được gọi khi xảy ra lỗi trong quá trình tải sản phẩm. Trả về `true` để thử tải lại. | | **didPartiallyLoadProducts** | tùy chọn | Được gọi khi sản phẩm được tải một phần. | | **showAlertItem** | tùy chọn | Một binding quản lý việc hiển thị các mục cảnh báo phía trên paywall. | | **showAlertBuilder** | tùy chọn | Một hàm để render view cảnh báo. | | **placeholderBuilder** | tùy chọn | Một hàm để render view placeholder trong khi paywall đang tải. | ## Xử lý sự kiện trong UIKit \{#handling-events-in-uikit\} Để kiểm soát hoặc theo dõi các tiến trình xảy ra trên màn hình paywall trong ứng dụng của bạn, hãy implement các phương thức `AdaptyPaywallControllerDelegate`. ### Sự kiện do người dùng tạo ra \{#user-generated-events\} #### Chọn sản phẩm \{#product-selection\} Khi người dùng chọn một sản phẩm để mua, phương thức này sẽ được gọi: ```swift showLineNumbers title="Swift" func paywallController( _ controller: AdaptyPaywallController, didSelectProduct product: AdaptyPaywallProductWithoutDeterminingOffer ) { } ```
Ví dụ sự kiện (Nhấp để mở rộng) ```javascript { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } ```
#### Bắt đầu mua hàng \{#started-purchase\} Nếu người dùng bắt đầu quá trình mua hàng, phương thức này sẽ được gọi: ```swift showLineNumbers title="Swift" func paywallController(_ controller: AdaptyPaywallController, didStartPurchase product: AdaptyPaywallProduct) { } ```
Ví dụ sự kiện (Nhấp để mở rộng) ```javascript { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } ```
Sự kiện này sẽ không được gọi trong chế độ Observer. Tham khảo chủ đề [iOS - Hiển thị paywall Paywall Builder trong chế độ Observer](ios-present-paywall-builder-paywalls-in-observer-mode) để biết thêm chi tiết. #### Bắt đầu mua hàng bằng web paywall \{#started-purchase-using-a-web-paywall\} Nếu người dùng khởi tạo quá trình mua hàng bằng cách sử dụng [web paywall](web-paywall), phương thức này sẽ được gọi: ```swift showLineNumbers title="Swift" func paywallController( _ controller: AdaptyPaywallController, shouldContinueWebPaymentNavigation product: AdaptyPaywallProduct ) { } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```javascript { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } ```
#### Mua hàng thành công hoặc đã hủy \{#successful-or-canceled-purchase\} Nếu mua hàng thành công, phương thức này sẽ được gọi: ```swift showLineNumbers title="Swift" func paywallController( _ controller: AdaptyPaywallController, didFinishPurchase product: AdaptyPaywallProductWithoutDeterminingOffer, purchaseResult: AdaptyPurchaseResult ) { } } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```javascript // Successful purchase { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "purchaseResult": { "type": "success", "profile": { "accessLevels": { "premium": { "id": "premium", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } } } } } // Cancelled purchase { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "purchaseResult": { "type": "cancelled" } } ```
Chúng tôi khuyến nghị đóng màn hình paywall trong trường hợp đó. Tính năng này sẽ không được gọi trong chế độ Observer. Xem thêm tại [iOS - Hiển thị paywall Paywall Builder trong chế độ Observer](ios-present-paywall-builder-paywalls-in-observer-mode) để biết chi tiết. #### Mua hàng thất bại \{#failed-purchase\} Nếu một giao dịch mua thất bại do lỗi, phương thức này sẽ được gọi. Điều này bao gồm các lỗi StoreKit (hạn chế thanh toán, sản phẩm không hợp lệ, lỗi mạng), lỗi xác minh giao dịch và lỗi hệ thống. Lưu ý rằng khi người dùng hủy, `didFinishPurchase` sẽ được gọi với kết quả đã hủy thay thế, và các thanh toán đang chờ xử lý không kích hoạt phương thức này. ```swift showLineNumbers title="Swift" func paywallController( _ controller: AdaptyPaywallController, didFailPurchase product: AdaptyPaywallProduct, error: AdaptyError ) { } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```javascript { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "error": { "code": "purchase_failed", "message": "Purchase failed due to insufficient funds", "details": { "underlyingError": "Insufficient funds in account" } } } ```
Nó sẽ không được gọi trong chế độ Observer. Tham khảo chủ đề [iOS - Present Paywall Builder paywalls in Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode) để biết thêm chi tiết. #### Mua hàng thất bại khi dùng web paywall \{#failed-purchase-using-a-web-paywall\} Nếu `Adapty.openWebPaywall()` thất bại, phương thức này sẽ được gọi: ```swift showLineNumbers title="Swift" func paywallController( _ controller: AdaptyPaywallController, didFailWebPaymentNavigation product: AdaptyPaywallProduct, error: AdaptyError ) { } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```javascript { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "error": { "code": "web_payment_failed", "message": "Web payment navigation failed", "details": { "underlyingError": "Network connection error" } } } ```
#### Khôi phục thành công \{#successful-restore\} Nếu việc khôi phục giao dịch thành công, phương thức này sẽ được gọi: ```swift showLineNumbers title="Swift" func paywallController( _ controller: AdaptyPaywallController, didFinishRestoreWith profile: AdaptyProfile ) { } ```
Ví dụ sự kiện (Nhấp để mở rộng) ```javascript { "profile": { "accessLevels": { "premium": { "id": "premium", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } }, "subscriptions": [ { "vendorProductId": "premium_monthly", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } ] } } ```
Chúng tôi khuyến nghị đóng màn hình nếu người dùng có `accessLevel` yêu cầu. Tham khảo chủ đề [Trạng thái gói đăng ký](subscription-status) để biết cách kiểm tra. #### Khôi phục thất bại \{#failed-restore\} Nếu việc khôi phục giao dịch thất bại, phương thức này sẽ được gọi: ```swift showLineNumbers title="Swift" public func paywallController( _ controller: AdaptyPaywallController, didFailRestoreWith error: AdaptyError ) { } ```
Ví dụ về sự kiện (Nhấn để mở rộng) ```javascript { "error": { "code": "restore_failed", "message": "Purchase restoration failed", "details": { "underlyingError": "No previous purchases found" } } } ```
### Tải dữ liệu và hiển thị \{#data-fetching-and-rendering\} #### Lỗi tải sản phẩm \{#product-loading-errors\} Nếu bạn không truyền mảng sản phẩm trong quá trình khởi tạo, AdaptyUI sẽ tự động lấy các đối tượng cần thiết từ server. Nếu thao tác này thất bại, AdaptyUI sẽ báo lỗi bằng cách gọi phương thức sau: ```swift showLineNumbers title="Swift" public func paywallController( _ controller: AdaptyPaywallController, didFailLoadingProductsWith error: AdaptyError ) -> Bool { return true } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```javascript { "error": { "code": "products_loading_failed", "message": "Failed to load products from the server", "details": { "underlyingError": "Network timeout" } } } ```
Nếu bạn trả về `true`, AdaptyUI sẽ lặp lại yêu cầu sau 2 giây. #### Lỗi khi render \{#rendering-errors\} Nếu có lỗi xảy ra trong quá trình render giao diện, lỗi đó sẽ được báo cáo qua phương thức này: ```swift showLineNumbers title="Swift" public func paywallController( _ controller: AdaptyPaywallController, didFailRenderingWith error: AdaptyError ) { } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```javascript { "error": { "code": "rendering_failed", "message": "Failed to render paywall interface", "details": { "underlyingError": "Invalid paywall configuration" } } } ```
Trong điều kiện bình thường, các lỗi này không nên xảy ra, vì vậy nếu bạn gặp phải, hãy cho chúng tôi biết.
--- # File: ios-use-fallback-paywalls --- --- title: "iOS - Sử dụng fallbacks" description: "Xử lý các trường hợp người dùng ngoại tuyến hoặc máy chủ Adapty không khả dụng" --- Để duy trì trải nghiệm người dùng mượt mà, điều quan trọng là phải thiết lập [paywall dự phòng](/fallback-paywalls) cho các flow, [paywall](paywalls) và [onboarding](onboardings) của bạn. Biện pháp phòng ngừa này giúp mở rộng khả năng của ứng dụng trong trường hợp mất kết nối internet một phần hoặc hoàn toàn. * **Nếu ứng dụng không thể kết nối đến máy chủ Adapty:** Ứng dụng vẫn có thể hiển thị flow hoặc paywall dự phòng, và truy cập cấu hình onboarding đã lưu cục bộ. * **Nếu ứng dụng không thể kết nối internet:** Ứng dụng vẫn có thể hiển thị flow hoặc paywall dự phòng. Onboarding chứa nội dung từ xa và cần có kết nối internet để hoạt động. :::important Trước khi thực hiện các bước trong hướng dẫn này, hãy [tải xuống](/local-fallback-paywalls) các file cấu hình dự phòng từ Adapty. ::: ## Cấu hình \{#configuration\} 1. Thêm file JSON fallback vào bundle dự án của bạn: mở menu **File** trong XCode và chọn tùy chọn **Add Files to "YourProjectName"**. 2. Gọi phương thức `.setFallback` **trước khi** bạn tải flow, paywall, hoặc onboarding mục tiêu. ```swift showLineNumbers do { if let urlPath = Bundle.main.url(forResource: fileName, withExtension: "json") { try await Adapty.setFallback(fileURL: urlPath) } } catch { // handle the error } ``` ```swift showLineNumbers if let url = Bundle.main.url(forResource: "ios_fallback", withExtension: "json") { Adapty.setFallback(fileURL: url) } ``` Tham số: | Tham số | Mô tả | | :---------- |:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **fileURL** | Đường dẫn đến file cấu hình fallback. | --- # File: localizations-and-locale-codes --- --- title: "Sử dụng localizations và locale codes trong iOS SDK" description: "Quản lý app localizations và locale codes để tiếp cận người dùng toàn cầu trong ứng dụng iOS của bạn." --- ## Tại sao điều này quan trọng \{#why-this-is-important\} Có một vài tình huống mà locale codes phát huy tác dụng — ví dụ, khi bạn muốn lấy đúng paywall cho localization hiện tại của ứng dụng. Vì locale codes khá phức tạp và có thể khác nhau tùy nền tảng, chúng tôi sử dụng một tiêu chuẩn nội bộ cho tất cả các nền tảng được hỗ trợ. Tuy nhiên, chính vì sự phức tạp đó, bạn cần hiểu rõ mình đang gửi gì lên server để nhận đúng localization — từ đó luôn nhận được kết quả như mong muốn. ## Tiêu chuẩn locale code tại Adapty \{#locale-code-standard-at-adapty\} Adapty sử dụng phiên bản được điều chỉnh nhẹ của [chuẩn BCP 47](https://en.wikipedia.org/wiki/IETF_language_tag): mỗi code gồm các subtag viết thường, phân cách bằng dấu gạch ngang. Ví dụ: `en` (tiếng Anh), `pt-br` (tiếng Bồ Đào Nha (Brazil)), `zh` (tiếng Trung giản thể), `zh-hant` (tiếng Trung phồn thể). ## Đối khớp locale code \{#locale-code-matching\} Khi Adapty nhận được lệnh gọi từ SDK phía client kèm theo locale code và bắt đầu tìm localization tương ứng của paywall, quá trình diễn ra như sau: 1. Chuỗi locale đầu vào được chuyển thành chữ thường và tất cả dấu gạch dưới (`_`) được thay bằng dấu gạch ngang (`-`) 2. Hệ thống tìm kiếm localization có locale code khớp hoàn toàn 3. Nếu không tìm thấy, hệ thống lấy chuỗi con trước dấu gạch ngang đầu tiên (`pt` từ `pt-br`) và tìm localization phù hợp 4. Nếu vẫn không tìm thấy, hệ thống trả về localization mặc định `en` Nhờ vậy, một thiết bị iOS gửi `'pt_BR'`, một thiết bị Android gửi `pt-BR`, và một thiết bị khác gửi `pt-br` đều nhận được cùng một kết quả. ## Triển khai localizations: cách được khuyến nghị \{#implementing-localizations-recommended-way\} Nếu bạn đang quan tâm đến localizations, nhiều khả năng bạn đã làm việc với các file localized string trong dự án. Nếu vậy, chúng tôi khuyến nghị đặt một cặp key-value chứa Adapty locale code tương ứng vào từng file localization. Sau đó, trích xuất giá trị của key đó khi gọi SDK, như sau: ```swift showLineNumbers // 1. Modify your Localizable.strings files /* Localizable.strings - Spanish */ adapty_paywalls_locale = "es"; /* Localizable.strings - Portuguese (Brazil) */ adapty_paywalls_locale = "pt-br"; // 2. Extract and use the locale code let locale = NSLocalizedString("adapty_paywalls_locale", comment: "") // pass locale code to AdaptyUI.getViewConfiguration or Adapty.getPaywall method ``` Cách này giúp bạn kiểm soát hoàn toàn localization nào sẽ được lấy cho từng người dùng của ứng dụng. ## Triển khai localizations: cách khác \{#implementing-localizations-the-other-way\} Bạn có thể đạt được kết quả tương tự (nhưng không hoàn toàn giống) mà không cần định nghĩa rõ locale code cho từng localization. Điều đó có nghĩa là trích xuất locale code từ các đối tượng khác mà nền tảng cung cấp, như thế này: ```swift showLineNumbers let locale = Locale.current.identifier // pass locale code to AdaptyUI.getViewConfiguration or Adapty.getPaywall method ``` Lưu ý rằng chúng tôi không khuyến nghị cách này vì một số lý do: 1. Trên iOS, ngôn ngữ ưa thích và locale hiện tại không giống nhau. Nếu muốn localization được chọn đúng, bạn phải hoặc dựa vào logic của Apple — vốn hoạt động tốt nếu bạn dùng cách được khuyến nghị với các file localized string — hoặc tự tái tạo lại logic đó. 2. Khó dự đoán chính xác server của Adapty sẽ nhận được gì. Ví dụ, trên iOS, có thể thu được locale như `ar_OM@numbers='latn'` trên thiết bị và gửi lên server. Với lệnh gọi này, bạn sẽ không nhận được localization `ar-om` như mong muốn, mà thay vào đó là `ar` — điều này có thể nằm ngoài dự tính. Nếu bạn vẫn quyết định dùng cách này — hãy đảm bảo đã xử lý hết tất cả các trường hợp liên quan. --- # File: ios-troubleshoot-paywall-builder --- --- title: "Khắc phục sự cố Paywall Builder trong iOS SDK" description: "Khắc phục sự cố Paywall Builder trong iOS SDK" --- Hướng dẫn này giúp bạn giải quyết các sự cố thường gặp khi sử dụng paywall được thiết kế trong Adapty Paywall Builder trên iOS SDK. ## Lấy cấu hình paywall thất bại \{#getting-a-paywall-configuration-fails\} **Sự cố**: Phương thức `getPaywallConfiguration` không thể lấy cấu hình paywall. **Nguyên nhân**: Paywall chưa được bật để hiển thị trên thiết bị trong Paywall Builder. **Giải pháp**: Bật toggle **Show on device** trong Paywall Builder. ## Số lượt xem paywall quá lớn \{#the-paywall-view-number-is-too-big\} **Sự cố**: Số lượt xem paywall hiển thị gấp đôi so với mong đợi. **Nguyên nhân**: Bạn có thể đang gọi `logShowFlow` (iOS SDK v4+) / `logShowPaywall` trong code, khiến số lượt xem bị tính hai lần nếu bạn đang dùng Paywall Builder hoặc Flow Builder. Với các flow và paywall được xây dựng bằng những công cụ này, analytics được theo dõi tự động, nên bạn không cần gọi phương thức này. **Giải pháp**: Đảm bảo bạn không gọi `logShowFlow` (iOS SDK v4+) / `logShowPaywall` trong code nếu đang sử dụng Paywall Builder hoặc Flow Builder. ## Các sự cố khác \{#other-issues\} **Sự cố**: Bạn gặp phải các vấn đề liên quan đến Paywall Builder khác không được đề cập ở trên. **Giải pháp**: Migrate SDK lên phiên bản mới nhất bằng cách sử dụng [hướng dẫn migration](ios-sdk-migration-guides) nếu cần. Nhiều sự cố đã được khắc phục trong các phiên bản SDK mới hơn. --- # File: ios-present-paywall-builder-paywalls-in-observer-mode --- --- title: "Hiển thị paywall Paywall Builder trong Observer mode trên iOS SDK" description: "Tìm hiểu cách hiển thị paywall PB trong observer mode để có thêm thông tin chi tiết." --- Nếu bạn đã tùy chỉnh paywall bằng Paywall Builder, bạn không cần phải lo lắng về việc render nó trong code ứng dụng để hiển thị cho người dùng. Paywall đó đã bao gồm cả nội dung lẫn cách thức hiển thị. :::warning Phần này chỉ áp dụng cho [Observer mode](observer-vs-full-mode). Nếu bạn không làm việc trong Observer mode, hãy tham khảo [iOS - Hiển thị paywall Paywall Builder](ios-present-paywalls). :::
Trước khi bắt đầu hiển thị flow (Click để mở rộng) 1. Thiết lập tích hợp ban đầu của Adapty [với App Store](initial_ios). 2. Cài đặt và cấu hình Adapty SDK. Đảm bảo đặt tham số `observerMode` thành `true`. Tham khảo [hướng dẫn cài đặt iOS SDK](sdk-installation-ios#activate-adapty-module-of-adapty-sdk). 3. [Tạo sản phẩm](create-product) trong Adapty Dashboard. 4. [Cấu hình flow hoặc paywall trong các builder](create-paywall) và gán sản phẩm cho chúng. 5. [Tạo placement và gán flow hoặc paywall vào đó](create-placement). 6. [Lấy flow và cấu hình của chúng](get-pb-paywalls) trong code ứng dụng.

1. Implement đối tượng `AdaptyObserverModeResolver`. Protocol này giống như trong SDK v3 — observer mode không thay đổi giữa việc render flow và paywall: ```swift showLineNumbers title="Swift" func observerMode(didInitiatePurchase product: AdaptyPaywallProduct, onStartPurchase: @escaping () -> Void, onFinishPurchase: @escaping () -> Void) { // use the product object to handle the purchase // call onStartPurchase / onFinishPurchase to notify AdaptyUI about the purchase progress } func observerModeDidInitiateRestorePurchases(onStartRestore: @escaping () -> Void, onFinishRestore: @escaping () -> Void) { // call onStartRestore / onFinishRestore to notify AdaptyUI about the restore progress } ``` 2. Tạo đối tượng cấu hình flow, truyền resolver của bạn vào tham số `observerModeResolver:`: ```swift showLineNumbers title="Swift" do { let flowConfiguration = try await AdaptyUI.getFlowConfiguration( forFlow: flow, observerModeResolver: ) } catch { // handle the error } ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :----------------------- | :------- | :----------------------------------------------------------------------------------------------------------------------- | | **forFlow** | bắt buộc | Đối tượng `AdaptyFlow` lấy qua `Adapty.getFlow(placementId:)`. Xem [Lấy flow và paywall](get-pb-paywalls). | | **observerModeResolver** | bắt buộc | `AdaptyObserverModeResolver` bạn đã implement ở trên. | 3. Khởi tạo flow controller bằng `AdaptyUI.flowController(with:delegate:)`: ```swift showLineNumbers title="Swift" import AdaptyUI let visualFlow = try AdaptyUI.flowController( with: flowConfiguration, delegate: ) ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :------------------------- | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------- | | **flowConfiguration** | bắt buộc | Đối tượng `AdaptyUI.FlowConfiguration` chứa thông tin hiển thị của flow. Xem [Lấy flow và paywall](get-pb-paywalls). | | **delegate** | bắt buộc | `AdaptyFlowControllerDelegate` để lắng nghe các sự kiện của flow. Xem [Xử lý sự kiện flow & paywall](ios-handling-events). | Giá trị trả về: | Đối tượng | Mô tả | | :------------------- | :----------------------------------------------------- | | AdaptyFlowController | Đối tượng đại diện cho màn hình flow được yêu cầu. | 4. Hiển thị controller: ```swift showLineNumbers title="Swift" present(visualFlow, animated: true) ``` :::warning Đừng quên [liên kết paywall với giao dịch mua](report-transactions-observer-mode). Nếu không, Adapty sẽ không xác định được paywall nguồn của giao dịch mua. ::: Trong SwiftUI, lấy cấu hình flow cùng với resolver và truyền vào modifier `.flow`: ```swift showLineNumbers title="SwiftUI" @State var flowPresented = false @State var flowConfiguration: AdaptyUI.FlowConfiguration? var body: some View { Text("Hello, AdaptyUI!") .flow( isPresented: $flowPresented, flowConfiguration: flowConfiguration, didPerformAction: { action in switch action { case .close: flowPresented = false default: break } }, 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 } ) .task { flowConfiguration = try? await AdaptyUI.getFlowConfiguration( forFlow: flow, observerModeResolver: ) } } ``` Tham số `observerModeResolver:` trong `getFlowConfiguration` là thứ giúp flow được render tôn trọng logic mua hàng tùy chỉnh của bạn — bản thân modifier sử dụng các callback giống như full mode. :::warning Đừng quên [liên kết paywall với giao dịch mua](report-transactions-observer-mode). Nếu không, Adapty sẽ không xác định được paywall nguồn của giao dịch mua. :::
Trước khi bắt đầu hiển thị paywall (Click để mở rộng) 1. Thiết lập tích hợp ban đầu của Adapty [với Google Play](initial-android) và [với App Store](initial_ios). 2. Cài đặt và cấu hình Adapty SDK. Đảm bảo đặt tham số `observerMode` thành `true`. Tham khảo hướng dẫn theo framework cho [iOS](sdk-installation-ios#activate-adapty-module-of-adapty-sdk). 3. [Tạo sản phẩm](create-product) trong Adapty Dashboard. 4. [Cấu hình paywall, gán sản phẩm cho chúng](create-paywall) và tùy chỉnh bằng Paywall Builder trong Adapty Dashboard. 5. [Tạo placement và gán paywall vào đó](create-placement) trong Adapty Dashboard. 6. [Lấy paywall Paywall Builder và cấu hình của chúng](get-pb-paywalls) trong code ứng dụng.

1. Implement đối tượng `AdaptyObserverModeResolver`: ```swift showLineNumbers title="Swift" func observerMode(didInitiatePurchase product: AdaptyPaywallProduct, onStartPurchase: @escaping () -> Void, onFinishPurchase: @escaping () -> Void) { // use the product object to handle the purchase // use the onStartPurchase and onFinishPurchase callbacks to notify AdaptyUI about the process of the purchase } func observerModeDidInitiateRestorePurchases(onStartRestore: @escaping () -> Void, onFinishRestore: @escaping () -> Void) { // use the onStartRestore and onFinishRestore callbacks to notify AdaptyUI about the process of the restore } ``` Sự kiện `observerMode(didInitiatePurchase:onStartPurchase:onFinishPurchase:)` sẽ thông báo cho bạn rằng người dùng đã bắt đầu một giao dịch mua. Bạn có thể kích hoạt flow mua hàng tùy chỉnh của mình khi nhận được callback này. Sự kiện `observerModeDidInitiateRestorePurchases(onStartRestore:onFinishRestore:)` sẽ thông báo cho bạn rằng người dùng đã bắt đầu khôi phục. Bạn có thể kích hoạt flow khôi phục tùy chỉnh của mình khi nhận được callback này. Ngoài ra, hãy nhớ gọi các callback sau để thông báo cho AdaptyUI về tiến trình mua hàng hoặc khôi phục. Điều này cần thiết để paywall hoạt động đúng, chẳng hạn như hiển thị loader: | Callback | Mô tả | | :----------------- | :------------------------------------------------------------------------------- | | onStartPurchase() | Gọi callback này để thông báo cho AdaptyUI rằng việc mua hàng đã bắt đầu. | | onFinishPurchase() | Gọi callback này để thông báo cho AdaptyUI rằng việc mua hàng đã hoàn tất. | | onStartRestore() | Gọi callback này để thông báo cho AdaptyUI rằng việc khôi phục đã bắt đầu. | | onFinishRestore() | Gọi callback này để thông báo cho AdaptyUI rằng việc khôi phục đã hoàn tất. | 2. Tạo đối tượng cấu hình paywall: ```swift showLineNumbers title="Swift" do { let paywallConfiguration = try AdaptyUI.getPaywallConfiguration( forPaywall: , observerModeResolver: ) } catch { // handle the error } ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :----------------------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Paywall** | bắt buộc | Đối tượng `AdaptyPaywall` để lấy controller cho paywall mong muốn. | | **ObserverModeResolver** | bắt buộc | Đối tượng `AdaptyObserverModeResolver` bạn đã implement ở bước trước. | 3. Khởi tạo paywall trực quan muốn hiển thị bằng phương thức `.paywallController(for:products:viewConfiguration:delegate:)`: ```swift showLineNumbers title="Swift" import AdaptyUI let visualPaywall = AdaptyUI.paywallController( with: , delegate: ) ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :----------------------- | :------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Paywall Configuration** | bắt buộc | Đối tượng `AdaptyUI.PaywallConfiguration` chứa thông tin hiển thị của paywall. Sử dụng phương thức `AdaptyUI.getPaywallConfiguration(forPaywall:locale:)`. Tham khảo chủ đề [Lấy paywall Paywall Builder và cấu hình của chúng](get-pb-paywalls) để biết thêm chi tiết. | | **Delegate** | bắt buộc | `AdaptyPaywallControllerDelegate` để lắng nghe các sự kiện paywall. Tham khảo chủ đề [Xử lý sự kiện paywall](ios-handling-events) để biết thêm chi tiết. | Giá trị trả về: | Đối tượng | Mô tả | | :---------------------- | :--------------------------------------------------- | | AdaptyPaywallController | Đối tượng đại diện cho màn hình paywall được yêu cầu. | Sau khi đối tượng được tạo thành công, bạn có thể hiển thị nó như sau: ```swift showLineNumbers title="Swift" present(visualPaywall, animated: true) ``` :::warning Đừng quên [liên kết paywall với giao dịch mua](report-transactions-observer-mode). Nếu không, Adapty sẽ không xác định được paywall nguồn của giao dịch mua. ::: Để hiển thị paywall trực quan trên màn hình thiết bị, hãy sử dụng modifier `.paywall` trong SwiftUI: ```swift showLineNumbers title="SwiftUI" @State var paywallPresented = false var body: some View { Text("Hello, AdaptyUI!") .paywall( isPresented: $paywallPresented, paywallConfiguration: , didPerformAction: { action in switch action { case .close: paywallPresented = false default: // Handle other actions break } }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, didFailRendering: { error in paywallPresented = false } ) } ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :----------------------- | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Paywall Configuration** | bắt buộc | Đối tượng `AdaptyUI.PaywallConfiguration` chứa thông tin hiển thị của paywall. Sử dụng phương thức `AdaptyUI.getPaywallConfiguration(forPaywall:locale:)`. Tham khảo chủ đề [Lấy paywall Paywall Builder và cấu hình của chúng](get-pb-paywalls) để biết thêm chi tiết. | | **Products** | tùy chọn | Cung cấp một mảng các đối tượng `AdaptyPaywallProduct` để tối ưu thời gian hiển thị sản phẩm trên màn hình. Nếu truyền `nil`, AdaptyUI sẽ tự động lấy các sản phẩm cần thiết. | | **TagResolver** | tùy chọn | Xác định một dictionary các tag tùy chỉnh và giá trị tương ứng. Tag tùy chỉnh đóng vai trò placeholder trong nội dung paywall, được thay thế động bằng các chuỗi cụ thể để cá nhân hóa nội dung trong paywall. Tham khảo chủ đề Tag tùy chỉnh trong Paywall Builder để biết thêm chi tiết. | | **ObserverModeResolver** | tùy chọn | Đối tượng `AdaptyObserverModeResolver` bạn đã implement ở bước trước. | Tham số closure: | Tham số closure | Mô tả | | :------------------- | :-------------------------------------------------------------------------------- | | **didFinishRestore** | Được gọi nếu Adapty.restorePurchases() thành công. | | **didFailRestore** | Được gọi nếu Adapty.restorePurchases() thất bại. | | **didFailRendering** | Được gọi nếu xảy ra lỗi trong quá trình render giao diện. | Tham khảo chủ đề [iOS - Xử lý sự kiện](ios-handling-events) để biết các tham số closure khác. :::warning Đừng quên [liên kết paywall với giao dịch mua](report-transactions-observer-mode). Nếu không, Adapty sẽ không xác định được paywall nguồn của giao dịch mua. :::
Trước khi bắt đầu hiển thị paywall (Click để mở rộng) 1. Thiết lập tích hợp ban đầu của Adapty [với Google Play](initial-android) và [với App Store](initial_ios). 1. Cài đặt và cấu hình Adapty SDK. Đảm bảo đặt tham số `observerMode` thành `true`. Tham khảo hướng dẫn theo framework cho [iOS](sdk-installation-ios#activate-adapty-module-of-adapty-sdk), [React Native](sdk-installation-reactnative), [Flutter](sdk-installation-flutter#activate-adapty-module-of-adapty-sdk) và [Unity](sdk-installation-unity#activate-adapty-module-of-adapty-sdk). 2. [Tạo sản phẩm](create-product) trong Adapty Dashboard. 3. [Cấu hình paywall, gán sản phẩm cho chúng](create-paywall) và tùy chỉnh bằng Paywall Builder trong Adapty Dashboard. 4. [Tạo placement và gán paywall vào đó](create-placement) trong Adapty Dashboard. 5. [Lấy paywall Paywall Builder và cấu hình của chúng](get-pb-paywalls) trong code ứng dụng.

1. Implement đối tượng `AdaptyObserverModeDelegate`: ```swift showLineNumbers title="Swift" func paywallController(_ controller: AdaptyPaywallController, didInitiatePurchase product: AdaptyPaywallProduct, onStartPurchase: @escaping () -> Void, onFinishPurchase: @escaping () -> Void) { // use the product object to handle the purchase // use the onStartPurchase and onFinishPurchase callbacks to notify AdaptyUI about the process of the purchase } ``` Sự kiện `paywallController(_:didInitiatePurchase:onStartPurchase:onFinishPurchase:)` sẽ thông báo cho bạn rằng người dùng đã bắt đầu một giao dịch mua. Bạn có thể kích hoạt flow mua hàng tùy chỉnh của mình khi nhận được sự kiện này. Ngoài ra, hãy nhớ gọi các callback sau để thông báo cho AdaptyUI về tiến trình mua hàng. Điều này cần thiết để paywall hoạt động đúng, chẳng hạn như hiển thị loader: | Callback | Mô tả | | :--------------- | :------------------------------------------------------------------------------- | | onStartPurchase | Gọi callback này để thông báo cho AdaptyUI rằng việc mua hàng đã bắt đầu. | | onFinishPurchase | Gọi callback này để thông báo cho AdaptyUI rằng việc mua hàng đã hoàn tất. | 2. Khởi tạo paywall trực quan muốn hiển thị bằng phương thức `.paywallController(for:products:viewConfiguration:delegate:observerModeDelegate:)`: ```swift showLineNumbers title="Swift" import AdaptyUI let visualPaywall = AdaptyUI.paywallController( for: , products: , viewConfiguration: , delegate: observerModeDelegate: ) ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :----------------------- | :------- | :----------------------------------------------------------- | | **Paywall** | bắt buộc | Đối tượng `AdaptyPaywall` để lấy controller cho paywall mong muốn. | | **Products** | tùy chọn | Cung cấp một mảng các đối tượng `AdaptyPaywallProduct` để tối ưu thời gian hiển thị sản phẩm trên màn hình. Nếu truyền `nil`, AdaptyUI sẽ tự động lấy các sản phẩm cần thiết. | | **ViewConfiguration** | bắt buộc | Đối tượng `AdaptyUI.LocalizedViewConfiguration` chứa thông tin hiển thị của paywall. Sử dụng phương thức `AdaptyUI.getViewConfiguration(paywall:locale:)`. Tham khảo chủ đề [Lấy paywall Paywall Builder và cấu hình của chúng](get-pb-paywalls) để biết thêm chi tiết. | | **Delegate** | bắt buộc | `AdaptyPaywallControllerDelegate` để lắng nghe các sự kiện paywall. Tham khảo chủ đề [Xử lý sự kiện paywall](ios-handling-events) để biết thêm chi tiết. | | **ObserverModeDelegate** | bắt buộc | Đối tượng `AdaptyObserverModeDelegate` bạn đã implement ở bước trước. | | **TagResolver** | tùy chọn | Xác định một dictionary các tag tùy chỉnh và giá trị tương ứng. Tag tùy chỉnh đóng vai trò placeholder trong nội dung paywall, được thay thế động bằng các chuỗi cụ thể để cá nhân hóa nội dung trong paywall. Tham khảo chủ đề Tag tùy chỉnh trong Paywall Builder để biết thêm chi tiết. | Giá trị trả về: | Đối tượng | Mô tả | | :---------------------- | :--------------------------------------------------- | | AdaptyPaywallController | Đối tượng đại diện cho màn hình paywall được yêu cầu. | Sau khi đối tượng được tạo thành công, bạn có thể hiển thị nó như sau: ```swift showLineNumbers title="Swift" present(visualPaywall, animated: true) ``` :::warning Đừng quên [liên kết paywall với giao dịch mua](report-transactions-observer-mode). Nếu không, Adapty sẽ không xác định được paywall nguồn của giao dịch mua. ::: Để hiển thị paywall trực quan trên màn hình thiết bị, hãy sử dụng modifier `.paywall` trong SwiftUI: ```swift showLineNumbers title="SwiftUI" @State var paywallPresented = false var body: some View { Text("Hello, AdaptyUI!") .paywall( isPresented: $paywallPresented, paywall: , configuration: , didPerformAction: { action in switch action { case .close: paywallPresented = false default: // Handle other actions break } }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, didFailRendering: { error in paywallPresented = false }, observerModeDidInitiatePurchase: { product, onStartPurchase, onFinishPurchase in // use the product object to handle the purchase // use the onStartPurchase and onFinishPurchase callbacks to notify AdaptyUI about the process of the purchase }, ) } ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :---------------- | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Paywall** | bắt buộc | Đối tượng `AdaptyPaywall` để lấy controller cho paywall mong muốn. | | **Product** | tùy chọn | Cung cấp một mảng các đối tượng `AdaptyPaywallProduct` để tối ưu thời gian hiển thị sản phẩm trên màn hình. Nếu truyền `nil`, AdaptyUI sẽ tự động lấy các sản phẩm cần thiết. | | **Configuration** | bắt buộc | Đối tượng `AdaptyUI.LocalizedViewConfiguration` chứa thông tin hiển thị của paywall. Sử dụng phương thức `AdaptyUI.getViewConfiguration(paywall:locale:)`. Tham khảo chủ đề [Lấy paywall Paywall Builder và cấu hình của chúng](get-pb-paywalls) để biết thêm chi tiết. | | **TagResolver** | tùy chọn | Xác định một dictionary các tag tùy chỉnh và giá trị tương ứng. Tag tùy chỉnh đóng vai trò placeholder trong nội dung paywall, được thay thế động bằng các chuỗi cụ thể để cá nhân hóa nội dung trong paywall. Tham khảo chủ đề Tag tùy chỉnh trong paywall builder để biết thêm chi tiết. | Tham số closure: | Tham số closure | Mô tả | | :---------------------------------- | :-------------------------------------------------------------------------------- | | **didFinishRestore** | Được gọi nếu Adapty.restorePurchases() thành công. | | **didFailRestore** | Được gọi nếu Adapty.restorePurchases() thất bại. | | **didFailRendering** | Được gọi nếu xảy ra lỗi trong quá trình render giao diện. | | **observerModeDidInitiatePurchase** | Được gọi khi người dùng bắt đầu một giao dịch mua. | Tham khảo chủ đề [iOS - Xử lý sự kiện](ios-handling-events) để biết các tham số closure khác. :::warning Đừng quên [liên kết paywall với giao dịch mua](report-transactions-observer-mode). Nếu không, Adapty sẽ không xác định được paywall nguồn của giao dịch mua. :::
--- # File: ios-quickstart-manual --- --- title: "Kích hoạt mua hàng trong paywall tùy chỉnh của bạn trên iOS SDK" description: "Tích hợp Adapty SDK vào các paywall iOS tùy chỉnh của bạn để kích hoạt in-app purchase." --- Hướng dẫn này mô tả cách tích hợp Adapty vào các paywall tùy chỉnh của bạn. Giữ toàn quyền kiểm soát việc triển khai paywall, trong khi Adapty SDK tự động lấy sản phẩm, xử lý giao dịch mua mới và khôi phục các giao dịch trước đó. :::important **Hướng dẫn này dành cho các nhà phát triển đang triển khai paywall tùy chỉnh.** Nếu bạn muốn cách đơn giản nhất để kích hoạt mua hàng, hãy sử dụng [Adapty Flow Builder](ios-quickstart-paywalls). Với Flow Builder, bạn tạo flow trong trình chỉnh sửa trực quan không cần code, Adapty tự động xử lý toàn bộ logic mua hàng, và bạn có thể thử nghiệm các thiết kế khác nhau mà không cần phát hành lại ứng dụng. ::: ## Trước khi bắt đầu \{#before-you-start\} ### Thiết lập sản phẩm \{#set-up-products\} Để 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) – bất kỳ thứ gì người dùng có thể mua (gói đăng ký, consumable, quyền truy cập trọn đời) - [**Paywalls**](paywalls) – các cấu hình xác định sản phẩm nào sẽ được cung cấp. Trong Adapty, paywall là cách duy nhất để lấy sản phẩm, nhưng thiết kế này cho phép bạn thay đổi sản phẩm, giá cả và ưu đãi mà không cần chỉnh sửa code ứng dụng. - [**Placements**](placements) – nơi và thời điểm bạn hiển thị paywall trong ứng dụng (ví dụ: `main`, `onboarding`, `settings`). Bạn thiết lập paywall cho các placement trong dashboard, sau đó yêu cầu chúng theo placement ID trong code. Điều này giúp dễ dàng chạy A/B test và hiển thị các paywall khác nhau cho các đối tượng người dùng khác nhau. Hãy đảm bảo bạn hiểu các khái niệm này ngay cả khi làm việc với paywall tùy chỉnh. Về cơ bản, đây chỉ là cách bạn quản lý các sản phẩm bán trong ứng dụng. Để triển khai paywall tùy chỉnh, bạn cần tạo một **paywall** và thêm nó vào một **placement**. Thiết lập này cho phép bạn lấy các sản phẩm. Để hiểu những gì cần làm trong dashboard, hãy xem hướng dẫn nhanh [tại đây](quickstart). ### Quản lý người dùng \{#manage-users\} Bạn có thể làm việc có hoặc không có xác thực backend ở phía bạn. Tuy nhiên, Adapty SDK xử lý người dùng ẩn danh và người dùng đã xác định theo cách khác nhau. Đọc [hướng dẫn nhanh về xác định người dùng](ios-quickstart-identify) để hiểu các đặc thù và đảm bảo bạn đang làm việc đúng cách với người dùng. ## Bước 1. Lấy sản phẩm \{#step-1-get-products\} Để lấy sản phẩm cho paywall tùy chỉnh của bạn, bạn cần: 1. Lấy đối tượng `flow` bằng cách truyền [placement](placements) ID vào phương thức `getFlow`. 2. Lấy mảng sản phẩm cho flow này bằng phương thức `getPaywallProducts`. ```swift func loadPaywall() async { do { let flow = try await Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID") let products = try await Adapty.getPaywallProducts(flow: flow) // Use products to build your custom paywall UI } catch { // Handle the error } } ``` ```swift func loadPaywall() { Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID") { result in switch result { case let .success(flow): Adapty.getPaywallProducts(flow: flow) { result in switch result { case let .success(products): // Use products to build your custom paywall UI case let .failure(error): // Handle the error } } case let .failure(error): // Handle the error } } } ``` ## Bước 2. Chấp nhận thanh toán \{#step-2-accept-purchases\} Khi người dùng nhấn vào một sản phẩm trong paywall tùy chỉnh của bạn, hãy gọi phương thức `makePurchase` với sản phẩm được chọn. Phương thức này sẽ xử lý flow mua hàng và trả về hồ sơ người dùng đã được cập nhật. ```swift func purchaseProduct(_ product: AdaptyPaywallProduct) async { do { let purchaseResult = try await Adapty.makePurchase(product: product) switch purchaseResult { case .userCancelled: // User canceled the purchase break case .pending: // Purchase is pending (e.g., awaiting parental approval) break case let .success(profile, transaction): // Purchase successful, profile updated break } } catch { // Handle the error } } ``` ```swift func purchaseProduct(_ product: AdaptyPaywallProduct) { Adapty.makePurchase(product: product) { result in switch result { case let .success(purchaseResult): switch purchaseResult { case .userCancelled: // User canceled the purchase break case .pending: // Purchase is pending (e.g., awaiting parental approval) break case let .success(profile, transaction): // Purchase successful, profile updated break } case let .failure(error): // Handle the error } } } ``` ## Bước 3. Khôi phục giao dịch \{#step-3-restore-purchases\} Apple yêu cầu tất cả ứng dụng có gói đăng ký phải cung cấp cách để người dùng khôi phục giao dịch của họ. Mặc dù giao dịch được tự động khôi phục khi người dùng đăng nhập bằng Apple ID, bạn vẫn phải triển khai nút khôi phục trong ứng dụng. Gọi phương thức `restorePurchases` khi người dùng nhấn nút khôi phục. Phương thức này sẽ đồng bộ lịch sử giao dịch của họ với Adapty và trả về hồ sơ người dùng đã được cập nhật. ```swift func restorePurchases() async { do { let profile = try await Adapty.restorePurchases() // Restore successful, profile updated } catch { // Handle the error } } ``` ```swift func restorePurchases() { Adapty.restorePurchases { result in switch result { case let .success(profile): // Restore successful, profile updated case let .failure(error): // Handle the error } } } ``` ## Các bước tiếp theo \{#next-steps\} --- no_index: true --- import Callout from '../../../components/Callout.astro'; 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 đỡ! Paywall của bạn đã sẵn sàng để hiển thị trong ứng dụng. [Kiểm tra giao dịch của bạn trong chế độ sandbox](test-purchases-in-sandbox) để đảm bảo bạn có thể hoàn thành một giao dịch thử nghiệm từ paywall. Tiếp theo, [kiểm tra xem người dùng đã hoàn thành giao dịch chưa](ios-check-subscription-status) để xác định xem có nên hiển thị paywall hay cấp quyền truy cập vào các tính năng trả phí. --- # File: fetch-paywalls-and-products --- --- title: "Lấy paywall và sản phẩm cho paywall remote config trong iOS SDK" description: "Lấy paywall và sản phẩm trong Adapty iOS SDK để tăng cường monetization cho người dùng." --- Trước khi hiển thị Remote Config và các paywall tùy chỉnh, bạn cần lấy thông tin về chúng. Lưu ý rằng phần này đề cập đến Remote Config và paywall tùy chỉnh. Để biết hướng dẫn lấy flow hoặc paywall được tùy chỉnh trong **Flow Builder** hoặc **Paywall Builder**, vui lòng tham khảo [iOS](get-pb-paywalls), [Android](android-get-pb-paywalls), [React Native](react-native-get-pb-paywalls), [Flutter](flutter-get-pb-paywalls), và [Unity](unity-get-pb-paywalls). :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. :::
Trước khi bắt đầu lấy flow và sản phẩm trong ứng dụng mobile của bạn (nhấn để mở rộng) 1. [Tạo sản phẩm của bạn](create-product) trong Adapty Dashboard. 2. [Tạo flow hoặc paywall và thêm sản phẩm vào đó](create-paywall) trong Adapty Dashboard. 3. [Tạo placement và thêm flow hoặc paywall vào placement](create-placement) trong Adapty Dashboard. 4. [Cài đặt Adapty SDK](sdk-installation-ios) trong ứng dụng mobile của bạn.
## Lấy thông tin flow \{#fetch-flow-information\} Trong Adapty, một [sản phẩm](product) là sự kết hợp của các sản phẩm từ cả App Store và Google Play. Các sản phẩm đa nền tảng này được tích hợp vào các flow và paywall, cho phép bạn hiển thị chúng tại các placement cụ thể trong ứng dụng di động. Để hiển thị các sản phẩm, bạn cần lấy `AdaptyFlow` từ một trong các [placement](placements) của mình bằng phương thức `getFlow`. :::important **Đừng hardcode product ID.** ID duy nhất bạn cần hardcode là placement ID. Các flow được cấu hình từ xa, nên số lượng sản phẩm và ưu đãi có thể thay đổi bất cứ lúc nào. Ứng dụng của bạn phải xử lý những thay đổi này một cách linh hoạt—nếu hôm nay flow trả về hai sản phẩm và ngày mai trả về ba, hãy hiển thị tất cả mà không cần thay đổi code. ::: ```swift showLineNumbers do { let flow = try await Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID") // the requested flow } catch { // handle the error } ``` ```swift showLineNumbers Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID") { result in switch result { case let .success(flow): // the requested flow case let .failure(error): // handle the error } } ``` | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` |

Theo mặc định, SDK sẽ cố tải dữ liệu từ server và trả về dữ liệu đã cache nếu thất bại. Chúng tôi khuyến nghị phương án này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.

Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp kết nối internet không ổn định, hãy cân nhắc sử dụng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng họ sẽ có trải nghiệm tải nhanh hơn, bất kể kết nối internet của họ có kém ổn định đến đâu. Cache được cập nhật thường xuyên, nên hoàn toàn an toàn khi sử dụng trong suốt phiên làm việc để tránh các yêu cầu mạng.

Lưu ý rằng cache vẫn được giữ nguyên sau khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc thực hiện dọn dẹp thủ công.

Adapty SDK lưu trữ flow và paywall theo hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và [paywall dự phòng](fallback-paywalls). Chúng tôi cũng sử dụng CDN để tải flow và paywall nhanh hơn, cùng một server dự phòng độc lập trong trường hợp CDN không thể truy cập được.

| | **loadTimeout** | mặc định: 5 giây |

Giá trị này giới hạn thời gian chờ cho phương thức này. Nếu hết thời gian chờ, dữ liệu đã cache hoặc fallback cục bộ sẽ được trả về.

Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ muộn hơn một chút so với giá trị được chỉ định trong `loadTimeout`, do thao tác có thể bao gồm nhiều yêu cầu khác nhau bên dưới.

| :::note Trong v4, tham số `locale` đã được chuyển ra khỏi `getFlow` và sang `getFlowConfiguration` (chỉ dùng khi render với AdaptyUI). Đối với các paywall tùy chỉnh, tất cả các locale khả dụng được trả về cùng nhau trong `flow.remoteConfigs` — hãy chọn locale phù hợp với thiết bị của người dùng hoặc cài đặt trong ứng dụng của bạn. ::: Đừng hardcode ID sản phẩm! Vì các flow được cấu hình từ xa, các sản phẩm có sẵn, số lượng sản phẩm và các ưu đãi đặc biệt (như dùng thử miễn phí) có thể thay đổi theo thời gian. Hãy đảm bảo code của bạn xử lý được các tình huống này. Ví dụ: nếu ban đầu bạn lấy được 2 sản phẩm, ứng dụng nên hiển thị đúng 2 sản phẩm đó. Nhưng nếu sau này lấy được 3 sản phẩm, ứng dụng phải hiển thị cả 3 mà không cần thay đổi code. Thứ duy nhất bạn cần hardcode là placement ID. Các tham số phản hồi: | Tham số | Mô tả | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Flow | Một đối tượng `AdaptyFlow` chứa placement, các định danh (`id`, `variationId`), tên, mảng `remoteConfigs` (một mục cho mỗi ngôn ngữ được cấu hình) và cờ `hasViewConfiguration`. Để lấy sản phẩm cho flow, hãy gọi `getPaywallProducts(flow:)`. | ## Lấy sản phẩm \{#fetch-products\} Sau khi có flow, bạn có thể truy vấn mảng sản phẩm tương ứng với nó: ```swift showLineNumbers do { let products = try await Adapty.getPaywallProducts(flow: flow) // the requested products array } catch { // handle the error } ``` ```swift showLineNumbers Adapty.getPaywallProducts(flow: flow) { result in switch result { case let .success(products): // the requested products array case let .failure(error): // handle the error } } ``` Tham số phản hồi: | Tham số | Mô tả | | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Products | Danh sách các đối tượng [`AdaptyPaywallProduct`](https://swift.adapty.io/documentation/adapty/adaptypaywallproduct) bao gồm: mã định danh sản phẩm, tên sản phẩm, giá, đơn vị tiền tệ, thời hạn gói đăng ký và một số thuộc tính khác. | Khi tự thiết kế paywall, bạn sẽ cần truy cập các thuộc tính từ đối tượng [`AdaptyPaywallProduct`](https://swift.adapty.io/documentation/adapty/adaptypaywallproduct). Dưới đây là các thuộc tính được dùng phổ biến nhất — xem tài liệu được liên kết để biết đầy đủ thông tin về tất cả các thuộc tính có sẵn. | Thuộc tính | Mô tả | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Tiêu đề** | Để hiển thị tiêu đề của sản phẩm, sử dụng `product.localizedTitle`. Lưu ý rằng bản địa hóa dựa trên quốc gia cửa hàng mà người dùng đã chọn, không phải ngôn ngữ của thiết bị. | | **Giá** | Để hiển thị giá đã được bản địa hóa, sử dụng `product.localizedPrice`. Bản địa hóa này dựa trên thông tin ngôn ngữ của thiết bị. Bạn cũng có thể truy cập giá dưới dạng số bằng `product.price`. Giá trị sẽ được cung cấp theo đơn vị tiền tệ địa phương. Để lấy ký hiệu tiền tệ tương ứng, sử dụng `product.currencySymbol`. | | **Chu kỳ gói đăng ký** | Để hiển thị chu kỳ (ví dụ: tuần, tháng, năm,...), sử dụng `product.localizedSubscriptionPeriod`. Bản địa hóa này dựa trên ngôn ngữ của thiết bị. Để lấy chu kỳ gói đăng ký theo chương trình, sử dụng `product.subscriptionPeriod`. Từ đó bạn có thể truy cập enum `unit` để lấy đơn vị thời gian (tức là ngày, tuần, tháng, năm hoặc không xác định). Giá trị `numberOfUnits` sẽ cho bạn biết số lượng đơn vị chu kỳ. Ví dụ, với gói đăng ký theo quý, bạn sẽ thấy `.month` trong thuộc tính unit và `3` trong thuộc tính numberOfUnits. | | **Ưu đãi giới thiệu** | Để hiển thị huy hiệu hoặc chỉ số khác cho biết gói đăng ký có ưu đãi giới thiệu, hãy kiểm tra thuộc tính `product.subscriptionOffer`. Trong đối tượng này có các thuộc tính hữu ích sau:
• `offerType`: một enum với các giá trị `introductory`, `promotional` và `winBack`. Dùng thử miễn phí và gói đăng ký giảm giá ban đầu sẽ thuộc loại `introductory`.
• `price`: Giá giảm dưới dạng số. Với bản dùng thử miễn phí, giá trị này là `0`.
• `localizedPrice`: Giá đã được định dạng theo ngôn ngữ của người dùng.
• `localizedNumberOfPeriods`: một chuỗi được bản địa hóa theo ngôn ngữ của thiết bị, mô tả độ dài của ưu đãi. Ví dụ, ưu đãi dùng thử ba ngày sẽ hiển thị `3 days` trong trường này.
• `subscriptionPeriod`: Ngoài ra, bạn có thể lấy thông tin chi tiết về chu kỳ ưu đãi thông qua thuộc tính này. Cách hoạt động tương tự như mô tả ở phần trước.
• `localizedSubscriptionPeriod`: Chu kỳ gói đăng ký đã được định dạng theo ngôn ngữ của người dùng. | :::note Trong v4, tất cả sản phẩm được trả về bởi `getPaywallProducts(flow:)` đã bao gồm thông tin về điều kiện áp dụng ưu đãi. Lời gọi `getPaywallProductsWithoutDeterminingOffer` riêng biệt từ v3 đã bị loại bỏ. ::: ## Tăng tốc tải flow với flow đối tượng mặc định \{#speed-up-flow-fetching-with-default-audience-flow\} Thông thường, flow được tải gần như ngay lập tức, vì vậy bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong các trường hợp bạn có nhiều đối tượng và placement, và người dùng có kết nối internet yếu, việc tải flow có thể mất nhiều thời gian hơn bạn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị một flow mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị gì cả. Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `getFlowForDefaultAudience`, phương thức này sẽ lấy flow của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy flow bằng phương thức `getFlow`, như đã mô tả chi tiết trong phần [Lấy thông tin flow](fetch-paywalls-and-products#fetch-flow-information) ở trên. :::warning Lý do chúng tôi khuyến nghị sử dụng `getFlow` Phương thức `getFlowForDefaultAudience` có một số hạn chế đáng kể: - **Các vấn đề tiềm ẩn về tương thích ngược**: Nếu bạn cần hiển thị các flow khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế các flow hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng đang dùng phiên bản hiện tại (cũ) có thể gặp sự cố với các flow không được hiển thị. - **Mất khả năng nhắm mục tiêu**: Tất cả người dùng sẽ thấy cùng một flow được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất khả năng nhắm mục tiêu cá nhân hóa (bao gồm theo quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của riêng bạn). Nếu bạn chấp nhận những hạn chế này để đổi lấy tốc độ tải flow nhanh hơn, hãy sử dụng phương thức `getFlowForDefaultAudience` như sau. Nếu không, hãy tiếp tục dùng `getFlow` đã mô tả [ở trên](fetch-paywalls-and-products#fetch-flow-information). ::: ```swift showLineNumbers do { let flow = try await Adapty.getFlowForDefaultAudience(placementId: "YOUR_PLACEMENT_ID") // the requested flow } catch { // handle the error } ``` ```swift showLineNumbers Adapty.getFlowForDefaultAudience(placementId: "YOUR_PLACEMENT_ID") { result in switch result { case let .success(flow): // the requested flow case let .failure(error): // handle the error } } ``` | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Mã định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` |

Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã cache nếu có lỗi xảy ra. Chúng tôi khuyến nghị dùng tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.

Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp tình trạng mạng không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng sẽ có tốc độ tải nhanh hơn bất kể kết nối mạng của họ kém đến đâu. Cache được cập nhật thường xuyên, vì vậy hoàn toàn an toàn khi dùng trong suốt phiên làm việc để tránh các yêu cầu mạng không cần thiết.

Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc xóa thủ công.

|
Trước khi hiển thị Remote Config và các paywall tùy chỉnh, bạn cần tải thông tin về chúng. Lưu ý rằng chủ đề này đề cập đến Remote Config và các paywall tùy chỉnh. Để biết hướng dẫn tải paywall cho các paywall được tùy chỉnh bằng Paywall Builder, vui lòng tham khảo [iOS](get-pb-paywalls), [Android](android-get-pb-paywalls), [React Native](react-native-get-pb-paywalls), [Flutter](flutter-get-pb-paywalls), và [Unity](unity-get-pb-paywalls). :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. :::
Trước khi bắt đầu tải paywall và sản phẩm trong ứng dụng mobile (nhấn để mở rộng) 1. [Tạo sản phẩm](create-product) trên Adapty Dashboard. 2. [Tạo paywall và thêm sản phẩm vào paywall](create-paywall) trên Adapty Dashboard. 3. [Tạo placement và thêm paywall vào placement](create-placement) trên Adapty Dashboard. 4. [Cài đặt Adapty SDK](sdk-installation-ios) trong ứng dụng mobile của bạn.
## Lấy thông tin paywall \{#fetch-paywall-information\} Trong Adapty, một [sản phẩm](product) là sự kết hợp của các sản phẩm từ cả App Store và Google Play. Các sản phẩm đa nền tảng này được tích hợp vào các paywall, cho phép bạn hiển thị chúng trong các placement cụ thể của ứng dụng di động. Để hiển thị các sản phẩm, bạn cần lấy một [Paywall](paywalls) từ một trong các [placement](placements) của bạn bằng phương thức `getPaywall`. :::important **Đừng hardcode ID sản phẩm.** ID duy nhất bạn nên hardcode là placement ID. Paywall được cấu hình từ xa, vì vậy số lượng sản phẩm và ưu đãi có thể thay đổi bất kỳ lúc nào. Ứng dụng của bạn phải xử lý những thay đổi này một cách linh hoạt—nếu hôm nay paywall trả về hai sản phẩm và ngày mai trả về ba, hãy hiển thị tất cả mà không cần thay đổi code. ::: ```swift showLineNumbers do { let paywall = try await Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID") // the requested paywall } catch { // handle the error } ``` ```swift showLineNumbers Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en") { result in switch result { case let .success(paywall): // the requested paywall case let .failure(error): // handle the error } } ``` | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** |

tùy chọn

mặc định: `en`

|

Định danh của [bản dịch paywall](add-remote-config-locale). Tham số này là mã ngôn ngữ gồm một hoặc nhiều thẻ con được phân tách bằng ký tự dấu trừ (**-**). Thẻ con đầu tiên là mã ngôn ngữ, thẻ con thứ hai là mã vùng.

Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha (Brazil).

Xem [Localizations and locale codes](localizations-and-locale-codes) để biết thêm thông tin về mã locale và cách chúng tôi khuyến nghị sử dụng chúng.

| | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` |

Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã cache nếu thất bại. Chúng tôi khuyến nghị phương án này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.

Tuy nhiên, nếu bạn cho rằng người dùng của mình thường gặp tình trạng kết nối internet không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu đã cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng thời gian tải sẽ nhanh hơn bất kể chất lượng kết nối. Cache được cập nhật thường xuyên nên hoàn toàn an toàn khi dùng trong phiên làm việc để tránh các yêu cầu mạng không cần thiết.

Lưu ý rằng cache vẫn được giữ nguyên sau khi khởi động lại ứng dụng và chỉ bị xóa khi cài đặt lại ứng dụng hoặc thực hiện dọn dẹp thủ công.

Adapty SDK lưu trữ paywall ở hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và [paywall dự phòng](fallback-paywalls). Chúng tôi cũng sử dụng CDN để tải paywall nhanh hơn và một máy chủ dự phòng độc lập trong trường hợp CDN không thể truy cập. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản mới nhất của paywall trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet hạn chế.

| | **loadTimeout** | mặc định: 5 giây |

Giá trị này giới hạn thời gian chờ cho phương thức này. Nếu hết thời gian chờ, dữ liệu đã cache hoặc fallback cục bộ sẽ được trả về.

Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể hết thời gian chờ muộn hơn một chút so với giá trị được chỉ định trong `loadTimeout`, do thao tác có thể bao gồm nhiều yêu cầu khác nhau bên dưới.

| Đừng hardcode product ID! Vì paywall được cấu hình từ xa, các sản phẩm có sẵn, số lượng sản phẩm và các ưu đãi đặc biệt (chẳng hạn như dùng thử miễn phí) có thể thay đổi theo thời gian. Hãy đảm bảo code của bạn xử lý được những trường hợp này. Ví dụ: nếu ban đầu bạn lấy được 2 sản phẩm, ứng dụng nên hiển thị đúng 2 sản phẩm đó. Nhưng nếu sau này lấy được 3 sản phẩm, ứng dụng cũng phải hiển thị cả 3 mà không cần thay đổi code. Thứ duy nhất bạn cần hardcode là placement ID. Tham số trả về: | Tham số | Mô tả | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | Một đối tượng [`AdaptyPaywall`](https://swift.adapty.io/documentation/adapty/adaptypaywall) gồm: danh sách ID sản phẩm, định danh paywall, Remote Config và một số thuộc tính khác. | ## Lấy sản phẩm \{#fetch-products\} Sau khi có paywall, bạn có thể truy vấn mảng sản phẩm tương ứng với paywall đó: ```swift showLineNumbers do { let products = try await Adapty.getPaywallProducts(paywall: paywall) // the requested products array } catch { // handle the error } ``` ```swift showLineNumbers Adapty.getPaywallProducts(paywall: paywall) { result in switch result { case let .success(products): // mảng sản phẩm được yêu cầu case let .failure(error): // xử lý lỗi } } ``` Các tham số phản hồi: | Tham số | Mô tả | | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Products | Danh sách các đối tượng [`AdaptyPaywallProduct`](https://swift.adapty.io/documentation/adapty/adaptypaywallproduct) bao gồm: mã định danh sản phẩm, tên sản phẩm, giá, đơn vị tiền tệ, thời hạn gói đăng ký và một số thuộc tính khác. | Khi tự thiết kế paywall, bạn sẽ cần truy cập các thuộc tính từ đối tượng [`AdaptyPaywallProduct`](https://swift.adapty.io/documentation/adapty/adaptypaywallproduct). Dưới đây là các thuộc tính được sử dụng phổ biến nhất — xem tài liệu được liên kết để biết đầy đủ chi tiết về tất cả các thuộc tính có sẵn. | Thuộc tính | Mô tả | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Tiêu đề** | Để hiển thị tiêu đề của sản phẩm, dùng `product.localizedTitle`. Lưu ý rằng việc bản địa hóa dựa trên quốc gia cửa hàng mà người dùng đã chọn, không phải ngôn ngữ của thiết bị. | | **Giá** | Để hiển thị giá đã được bản địa hóa, dùng `product.localizedPrice`. Việc bản địa hóa dựa trên thông tin ngôn ngữ của thiết bị. Bạn cũng có thể lấy giá dưới dạng số bằng `product.price`. Giá trị sẽ được tính theo đơn vị tiền tệ địa phương. Để lấy ký hiệu tiền tệ tương ứng, dùng `product.currencySymbol`. | | **Chu kỳ gói đăng ký** | Để hiển thị chu kỳ (ví dụ: tuần, tháng, năm,...), dùng `product.localizedSubscriptionPeriod`. Việc bản địa hóa dựa trên ngôn ngữ của thiết bị. Để lấy chu kỳ gói đăng ký theo lập trình, dùng `product.subscriptionPeriod`. Từ đó bạn có thể truy cập enum `unit` để lấy độ dài (tức là ngày, tuần, tháng, năm hoặc không xác định). Giá trị `numberOfUnits` sẽ cho bạn số lượng đơn vị chu kỳ. Ví dụ: với gói đăng ký theo quý, thuộc tính unit sẽ là `.month` và numberOfUnits sẽ là `3`. | | **Ưu đãi giới thiệu** | Để hiển thị badge hoặc chỉ báo khác cho biết gói đăng ký có ưu đãi giới thiệu, hãy kiểm tra thuộc tính `product.subscriptionOffer`. Trong object này có các thuộc tính hữu ích sau:
• `offerType`: enum với các giá trị `introductory`, `promotional` và `winBack`. Dùng thử miễn phí và gói đăng ký giảm giá ban đầu sẽ thuộc loại `introductory`.
• `price`: Giá ưu đãi dưới dạng số. Với dùng thử miễn phí, giá trị này sẽ là `0`.
• `localizedPrice`: Giá ưu đãi đã được định dạng theo ngôn ngữ của người dùng.
• `localizedNumberOfPeriods`: Chuỗi được bản địa hóa theo ngôn ngữ thiết bị, mô tả độ dài của ưu đãi. Ví dụ: ưu đãi dùng thử 3 ngày sẽ hiển thị `3 days` trong trường này.
• `subscriptionPeriod`: Ngoài ra, bạn có thể lấy thông tin chi tiết riêng lẻ của chu kỳ ưu đãi bằng thuộc tính này. Cách hoạt động giống như phần mô tả ở trên.
• `localizedSubscriptionPeriod`: Chu kỳ gói đăng ký ưu đãi đã được định dạng theo ngôn ngữ của người dùng. | ## Kiểm tra điều kiện nhận ưu đãi giới thiệu trên iOS \{#check-intro-offer-eligibility-on-ios\} Theo mặc định, phương thức `getPaywallProducts` sẽ kiểm tra điều kiện nhận ưu đãi giới thiệu, ưu đãi và ưu đãi thu hút khách hàng cũ. Nếu bạn cần hiển thị sản phẩm trước khi SDK xác định điều kiện nhận ưu đãi, hãy sử dụng phương thức `getPaywallProductsWithoutDeterminingOffer` thay thế. :::note Sau khi hiển thị các sản phẩm ban đầu, hãy nhớ gọi phương thức `getPaywallProducts` thông thường để cập nhật sản phẩm với thông tin điều kiện nhận ưu đãi chính xác. ::: ```swift showLineNumbers do { let products = try await Adapty.getPaywallProductsWithoutDeterminingOffer(paywall: paywall) // the requested products array without subscriptionOffer } catch { // handle the error } ``` ```swift showLineNumbers Adapty.getPaywallProductsWithoutDeterminingOffer(paywall: paywall) { result in switch result { case let .success(products): // mảng sản phẩm yêu cầu không có subscriptionOffer case let .failure(error): // xử lý lỗi } } ``` ## Tăng tốc độ tải paywall với paywall đối tượng mặc định \{#speed-up-paywall-fetching-with-default-audience-paywall\} Thông thường, paywall được tải gần như ngay lập tức, nên bạn không cần lo lắng về việc tối ưu tốc độ này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và paywall, cộng với kết nối internet yếu, việc tải paywall có thể mất nhiều thời gian hơn mong đợi. Trong những tình huống đó, bạn có thể muốn hiển thị một paywall mặc định để đảm bảo trải nghiệm người dùng mượt mà, thay vì không hiển thị gì cả. Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `getPaywallForDefaultAudience`, phương thức này sẽ lấy paywall của placement đã chỉ định dành cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị vẫn là lấy paywall bằng phương thức `getPaywall`, như đã mô tả chi tiết trong phần [Lấy thông tin paywall](fetch-paywalls-and-products#fetch-paywall-information) ở trên. :::warning Tại sao chúng tôi khuyến nghị sử dụng `getPaywall` Phương thức `getPaywallForDefaultAudience` có một số nhược điểm đáng kể: - **Vấn đề tương thích ngược tiềm ẩn**: Nếu bạn cần hiển thị các paywall khác nhau cho các phiên bản ứng dụng khác nhau (hiện tại và tương lai), bạn có thể gặp khó khăn. Bạn sẽ phải thiết kế các paywall hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng với phiên bản hiện tại (cũ) có thể gặp sự cố với các paywall không được hiển thị. - **Mất khả năng nhắm mục tiêu**: Tất cả người dùng sẽ thấy cùng một paywall được thiết kế cho đối tượng **All Users**, nghĩa là bạn mất khả năng nhắm mục tiêu cá nhân hóa (bao gồm dựa trên quốc gia, attribution marketing hoặc các thuộc tính tùy chỉnh của riêng bạn). Nếu bạn chấp nhận những hạn chế này để được hưởng lợi từ việc tải paywall nhanh hơn, hãy sử dụng phương thức `getPaywallForDefaultAudience` như sau. Nếu không, hãy tiếp tục dùng `getPaywall` được mô tả [ở trên](fetch-paywalls-and-products#fetch-paywall-information). ::: ```swift showLineNumbers do { let paywall = try await Adapty.getPaywallForDefaultAudience("YOUR_PLACEMENT_ID") // the requested paywall } catch { // handle the error } ``` ```swift showLineNumbers Adapty.getPaywallForDefaultAudience(placementId: "YOUR_PLACEMENT_ID", locale: "en") { result in switch result { case let .success(paywall): // the requested paywall case let .failure(error): // handle the error } } ``` :::note Phương thức `getPaywallForDefaultAudience` khả dụng từ iOS SDK phiên bản 2.11.2 trở lên. ::: | Tham số | Bắt buộc | Mô tả | |---------|--------|-----------| | **placementId** | bắt buộc | Định danh của [Placement](placements). Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** |

tùy chọn

mặc định: `en`

|

Định danh của [bản địa hóa paywall](add-remote-config-locale). Tham số này phải là mã ngôn ngữ gồm một hoặc nhiều thẻ con được phân tách bằng ký tự dấu trừ (**-**). Thẻ con đầu tiên là ngôn ngữ, thẻ con thứ hai là vùng.

Ví dụ: `en` nghĩa là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha của Brazil.

Xem [Localizations and locale codes](localizations-and-locale-codes) để biết thêm thông tin về mã locale và cách chúng tôi khuyến nghị sử dụng.

| | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` |

Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã lưu trong cache nếu thất bại. Chúng tôi khuyến nghị tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.

Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp tình trạng internet không ổn định, hãy cân nhắc dùng `.returnCacheDataElseLoad` để trả về dữ liệu cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng thời gian tải sẽ nhanh hơn dù kết nối internet kém ổn định. Cache được cập nhật thường xuyên, vì vậy hoàn toàn an toàn khi sử dụng trong phiên làm việc để tránh các yêu cầu mạng không cần thiết.

Lưu ý rằng cache vẫn được giữ nguyên sau khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt ứng dụng hoặc thực hiện dọn dẹp thủ công.

|
--- # File: present-remote-config-paywalls --- --- title: "Hiển thị paywall được thiết kế bằng remote config trong iOS SDK" description: "Khám phá cách trình bày paywall remote config trong Adapty để cá nhân hóa trải nghiệm người dùng." --- Nếu bạn đã tùy chỉnh paywall bằng Remote Config, bạn cần tự triển khai phần hiển thị trong code của ứng dụng để người dùng nhìn thấy nó. Vì Remote Config mang lại sự linh hoạt theo nhu cầu của bạn, bạn hoàn toàn quyết định nội dung bao gồm những gì và giao diện paywall trông như thế nào. Adapty cung cấp phương thức để lấy cấu hình remote, giúp bạn tự do trình bày paywall tùy chỉnh của mình. Đừng quên [kiểm tra xem người dùng có đủ điều kiện nhận ưu đãi giới thiệu trên iOS không](fetch-paywalls-and-products#check-intro-offer-eligibility-on-ios) và điều chỉnh giao diện paywall để xử lý trường hợp họ đủ điều kiện. ## Lấy remote config của flow và hiển thị \{#get-flow-remote-config-and-present-it\} Trong v4, một flow chứa một mục `AdaptyRemoteConfig` cho mỗi locale đã cấu hình trong mảng `remoteConfigs`. Chọn locale phù hợp với tùy chọn của người dùng, sau đó đọc các giá trị bạn cần. ```swift showLineNumbers do { let flow = try await Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID") let config = flow.remoteConfigs.first(where: { $0.locale == "en" }) ?? flow.remoteConfigs.first let headerText = config?.dictionary?["header_text"] as? String } catch { // handle the error } ``` ```swift showLineNumbers Adapty.getFlow(placementId: "YOUR_PLACEMENT_ID") { result in let flow = try? result.get() let config = flow?.remoteConfigs.first(where: { $0.locale == "en" }) ?? flow?.remoteConfigs.first let headerText = config?.dictionary?["header_text"] as? String } ``` Sau khi đã nhận được tất cả các giá trị cần thiết, đã đến lúc render và ghép chúng thành một trang hấp dẫn. Hãy đảm bảo thiết kế tương thích với nhiều kích thước màn hình và hướng hiển thị khác nhau, mang lại trải nghiệm mượt mà và thân thiện trên mọi thiết bị. :::warning Hãy chắc chắn [ghi lại sự kiện xem paywall](present-remote-config-paywalls#track-paywall-view-events) như mô tả bên dưới để Adapty analytics có thể thu thập dữ liệu cho funnel và A/B test. ::: Sau khi hoàn tất việc hiển thị paywall, tiếp tục thiết lập flow mua hàng. Khi người dùng thực hiện mua hàng, chỉ cần gọi `.makePurchase()` với sản phẩm từ flow của bạn. Để biết thêm chi tiết về phương thức `.makePurchase()`, hãy đọc [Thực hiện mua hàng](making-purchases). Chúng tôi khuyến nghị [tạo paywall dự phòng (fallback paywall)](fallback-paywalls). Paywall dự phòng này sẽ hiển thị cho người dùng khi không có kết nối internet hoặc không có cache, đảm bảo trải nghiệm mượt mà ngay cả trong những tình huống đó. ## Theo dõi sự kiện xem paywall \{#track-paywall-view-events\} Adapty giúp bạn đo lường hiệu quả của các paywall. Trong khi dữ liệu mua hàng được thu thập tự động, việc ghi lại lượt xem paywall cần bạn thực hiện thủ công vì chỉ bạn mới biết khi nào người dùng nhìn thấy paywall. Để ghi lại sự kiện xem paywall, chỉ cần gọi `.logShowFlow(flow)` — kết quả sẽ được phản ánh trong các chỉ số paywall của bạn trong funnel và A/B test. :::important Không cần gọi `.logShowFlow(flow)` nếu bạn đang hiển thị 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 lượt xem trong những trường hợp đó. ::: ```swift showLineNumbers try await Adapty.logShowFlow(flow) ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :-------- | :------- |:-----------------------------------------------------------------------------------------| | **flow** | bắt buộc | Đối tượng `AdaptyFlow` lấy được qua `Adapty.getFlow(placementId:)`. | Nếu bạn đã tùy chỉnh paywall bằng Remote Config, bạn cần tự triển khai phần hiển thị trong code của ứng dụng để người dùng nhìn thấy nó. Vì Remote Config mang lại sự linh hoạt theo nhu cầu của bạn, bạn hoàn toàn quyết định nội dung bao gồm những gì và giao diện paywall trông như thế nào. Chúng tôi cung cấp phương thức để lấy cấu hình remote, giúp bạn tự do trình bày paywall tùy chỉnh được cấu hình qua Remote Config. Đừng quên [kiểm tra xem người dùng có đủ điều kiện nhận ưu đãi giới thiệu trên iOS không](fetch-paywalls-and-products#check-intro-offer-eligibility-on-ios) và điều chỉnh giao diện paywall để xử lý trường hợp họ đủ điều kiện. ## Lấy remote config của paywall và hiển thị \{#get-paywall-remote-config-and-present-it\} Để lấy remote config của một paywall, truy cập thuộc tính `remoteConfig` và trích xuất các giá trị cần thiết. ```swift showLineNumbers do { let paywall = try await Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID") let headerText = paywall.remoteConfig?.dictionary?["header_text"] as? String } catch { // handle the error } ``` ```swift showLineNumbers Adapty.getPaywall(placementId: "YOUR_PLACEMENT_ID") { result in let paywall = try? result.get() let headerText = paywall?.remoteConfig?.dictionary?["header_text"] as? String } ``` Sau khi đã nhận được tất cả các giá trị cần thiết, đã đến lúc render và ghép chúng thành một trang hấp dẫn. Hãy đảm bảo thiết kế tương thích với nhiều kích thước màn hình và hướng hiển thị khác nhau, mang lại trải nghiệm mượt mà và thân thiện trên mọi thiết bị. :::warning Hãy chắc chắn [ghi lại sự kiện xem paywall](present-remote-config-paywalls#track-paywall-view-events) như mô tả bên dưới để Adapty analytics có thể thu thập dữ liệu cho funnel và A/B test. ::: Sau khi hoàn tất việc hiển thị paywall, tiếp tục thiết lập flow mua hàng. Khi người dùng thực hiện mua hàng, chỉ cần gọi `.makePurchase()` với sản phẩm từ paywall của bạn. Để biết thêm chi tiết về phương thức `.makePurchase()`, hãy đọc [Thực hiện mua hàng](making-purchases). Chúng tôi khuyến nghị [tạo paywall dự phòng (fallback paywall)](fallback-paywalls). Paywall dự phòng này sẽ hiển thị cho người dùng khi không có kết nối internet hoặc không có cache, đảm bảo trải nghiệm mượt mà ngay cả trong những tình huống đó. ## Theo dõi sự kiện xem paywall \{#track-paywall-view-events\} Adapty giúp bạn đo lường hiệu quả của các paywall. Trong khi dữ liệu mua hàng được thu thập tự động, việc ghi lại lượt xem paywall cần bạn thực hiện thủ công vì chỉ bạn mới biết khi nào người dùng nhìn thấy paywall. Để ghi lại sự kiện xem paywall, chỉ cần gọi `.logShowPaywall(paywall)` — kết quả sẽ được phản ánh trong các chỉ số paywall của bạn trong funnel và A/B test. :::important Không cần gọi `.logShowPaywall(paywall)` nếu bạn đang hiển thị paywall được tạo trong [paywall builder](adapty-paywall-builder). ::: ```swift showLineNumbers Adapty.logShowPaywall(paywall) ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :---------- | :------- |:-----------------------------------------------------------------------------------------| | **paywall** | bắt buộc | Đối tượng [`AdaptyPaywall`](https://swift.adapty.io/documentation/adapty/adaptypaywall). | --- # File: making-purchases --- --- title: "Thực hiện mua hàng trong ứng dụng trên iOS SDK" description: "Hướng dẫn xử lý in-app purchase và gói đăng ký bằng Adapty." --- Hiển thị paywall trong ứng dụng là bước thiết yếu để cung cấp cho người dùng quyền truy cập vào nội dung hoặc dịch vụ cao cấp. Tuy nhiên, chỉ cần hiển thị paywall là đủ để hỗ trợ mua hàng nếu bạn dùng [Paywall Builder](adapty-paywall-builder) để tùy chỉnh paywall của mình. Nếu bạn không dùng Paywall Builder, bạn phải sử dụng một phương thức riêng tên `.makePurchase()` để hoàn tất giao dịch và mở khóa nội dung mong muốn. Phương thức này là cổng để người dùng tương tác với paywall và tiến hành giao dịch. Nếu paywall của bạn có ưu đãi đang hoạt động cho sản phẩm người dùng đang muốn mua, Adapty sẽ tự động áp dụng ưu đãi đó tại thời điểm mua hàng. :::warning Lưu ý rằng ưu đãi giới thiệu chỉ được áp dụng tự động nếu bạn sử dụng paywall được thiết lập bằng Paywall Builder. Trong các trường hợp khác, bạn cần [xác minh tư cách đủ điều kiện nhận ưu đãi giới thiệu của người dùng trên iOS](fetch-paywalls-and-products#check-intro-offer-eligibility-on-ios). Bỏ qua bước này có thể dẫn đến việc ứng dụng bị từ chối khi phát hành, đồng thời có thể tính giá đầy đủ cho những người dùng đủ điều kiện nhận ưu đãi giới thiệu. ::: Hãy đảm bảo bạn đã [hoàn tất cấu hình ban đầu](quickstart) mà không bỏ qua bất kỳ bước nào. Nếu không, chúng tôi không thể xác thực giao dịch mua hàng. ## Thực hiện mua hàng \{#make-purchase\} :::note **Đang dùng [Paywall Builder](adapty-paywall-builder)?** Giao dịch được xử lý tự động — bạn có thể bỏ qua bước này. **Cần hướng dẫn từng bước?** Xem [hướng dẫn quickstart](ios-implement-paywalls-manually) để có hướng dẫn triển khai đầy đủ từ đầu đến cuối. ::: ```swift showLineNumbers do { let purchaseResult = try await Adapty.makePurchase(product: product) switch purchaseResult { case .userCancelled: // Handle the case where the user canceled the purchase case .pending: // Handle deferred purchases (e.g., the user will pay offline with cash) case let .success(profile, transaction): if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // Grant access to the paid features } } } catch { // Handle the error } ``` ```swift showLineNumbers Adapty.makePurchase(product: product) { result in switch result { case let .success(purchaseResult): switch purchaseResult { case .userCancelled: // Handle the case where the user canceled the purchase case .pending: // Handle deferred purchases (e.g., the user will pay offline with cash) case let .success(profile, transaction): if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // Grant access to the paid features } } case let .failure(error): // Handle the error } } ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :---------- | :------- | :-------------------------------------------------------------------------------------------------- | | **Product** | bắt buộc | Một đối tượng [`AdaptyPaywallProduct`](https://swift.adapty.io/documentation/adapty/adaptypaywallproduct) lấy từ paywall. | Tham số phản hồi: | Tham số | Mô tả | |---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Profile** |

Nếu yêu cầu thành công, phản hồi sẽ chứa đối tượng này. Đối tượng [AdaptyProfile](https://swift.adapty.io/documentation/adapty/adaptyprofile) cung cấp thông tin toàn diện về các mức độ truy cập, gói đăng ký và sản phẩm mua một lần của người dùng trong ứng dụng.

Kiểm tra trạng thái mức độ truy cập để xác định xem người dùng có quyền truy cập cần thiết vào ứng dụng hay không.

| :::warning **Lưu ý:** nếu bạn vẫn đang dùng StoreKit của Apple phiên bản thấp hơn v2.0 và Adapty SDK phiên bản thấp hơn v2.9.0, bạn cần cung cấp [Apple App Store shared secret](app-store-connection-configuration#step-5-enter-app-store-shared-secret) thay thế. Phương thức này hiện đã bị Apple ngừng hỗ trợ. ::: ## In-app purchase từ App Store \{#in-app-purchases-from-the-app-store\} Khi người dùng bắt đầu mua hàng trong App Store và giao dịch được chuyển sang ứng dụng của bạn, bạn có hai lựa chọn: - **Xử lý giao dịch ngay lập tức:** Trả về `true` trong `shouldAddStorePayment`. Thao tác này sẽ hiển thị màn hình mua hàng của Apple ngay lập tức. - **Lưu đối tượng sản phẩm để xử lý sau:** Trả về `false` trong `shouldAddStorePayment`, sau đó gọi `makePurchase` với sản phẩm đã lưu vào lúc thích hợp. Điều này có thể hữu ích nếu bạn cần hiển thị nội dung tùy chỉnh cho người dùng trước khi kích hoạt giao dịch mua hàng. Đây là đoạn code hoàn chỉnh: ```swift showLineNumbers title="Swift" final class YourAdaptyDelegateImplementation: AdaptyDelegate { nonisolated func shouldAddStorePayment(for product: AdaptyDeferredProduct) -> Bool { // 1a. // Return `true` to continue the transaction in your app. The Apple purchase system screen will show automatically. // 1b. // Store the product object and return `false` to defer or cancel the transaction. false } // 2. Continue the deferred purchase later on by passing the product to `makePurchase` when the timing is appropriate func continueDeferredPurchase() async { let storedProduct: AdaptyDeferredProduct = // get the product object from 1b. do { try await Adapty.makePurchase(product: storedProduct) } catch { // handle the error } } } ``` ## Đổi mã ưu đãi trên iOS \{#redeem-offer-codes-in-ios\} --- no_index: true --- import Callout from '../../../components/Callout.astro';
Về offer code Offer code cho phép bạn tặng ưu đãi hoặc dùng thử miễn phí cho những người dùng cụ thể. Không giống như các ưu đãi thông thường được áp dụng tự động, offer code được phân phối bên ngoài ứng dụng — qua email marketing, mạng xã hội, hoặc tài liệu in ấn. Người dùng đổi code bằng cách nhập vào App Store, truy cập URL đổi thưởng, hoặc qua hộp thoại trong ứng dụng. Để thiết lập offer code, mở một gói đăng ký trong App Store Connect và vào mục **Offer Codes**. Bạn có thể tạo [ba loại](https://developer.apple.com/help/app-store-connect/manage-subscriptions/set-up-subscription-offer-codes) offer code: - **Free** — gói đăng ký miễn phí trong một khoảng thời gian nhất định, sau đó gia hạn với giá đầy đủ. - **Pay as you go** — người dùng trả giá ưu đãi theo từng chu kỳ thanh toán trong một khoảng thời gian, sau đó gói đăng ký gia hạn với giá đầy đủ. - **Pay up front** — người dùng trả một lần với giá ưu đãi cho toàn bộ thời gian ưu đãi, sau đó gói đăng ký gia hạn với giá đầy đủ. Bạn không cần thêm offer code vào Adapty. Apple gắn tag mọi giao dịch trong thời gian ưu đãi với danh mục offer code. Điều này bao gồm lần đổi code đầu tiên và tất cả các lần gia hạn ưu đãi tiếp theo. Adapty phát hiện tag đó và ghi lại từng giao dịch với danh mục ưu đãi `offer_code`. Khi thời gian ưu đãi kết thúc và gói đăng ký gia hạn với giá đầy đủ, tag đó sẽ không còn nữa. Bạn có thể lọc analytics theo loại ưu đãi **Offer Code** trên [Adapty Dashboard](controls-filters-grouping-compare-proceeds). #### Xử lý sự chênh lệch doanh thu \{#revenue-discrepancy-troubleshooting\} Nếu bạn nhận thấy một giao dịch offer code xuất hiện trong Adapty với giá đầy đủ thay vì giá ưu đãi, hãy kiểm tra lại những điểm sau trong App Store Connect: - Offer code đã được cấu hình đúng giá cho tất cả các khu vực mà người dùng có thể đổi code. - Giá ưu đãi đã được thiết lập cho quốc gia hoặc khu vực cụ thể của người dùng. Apple gửi giá theo khu vực trong giao dịch. Nếu không có giá khu vực nào được cấu hình cho ưu đãi, Apple có thể gửi giá đầy đủ của sản phẩm. Bạn có thể lọc và kiểm tra các giao dịch offer code trên [Adapty Dashboard](controls-filters-grouping-compare-proceeds) theo loại ưu đãi **Offer Code** và bộ lọc **Offer Discount Type**. #### Promo code cũ (đã ngừng hỗ trợ) \{#legacy-promo-codes-deprecated\} Apple đã ngừng hỗ trợ promo code cho in-app purchase vào tháng 3 năm 2026. Offer code thay thế chúng với nhiều tính năng hơn: điều kiện tham gia có thể cấu hình, ngày hết hạn, và lên đến 1 triệu code mỗi quý. Nếu trước đây bạn sử dụng promo code cho in-app purchase, hãy chuyển sang offer code trong App Store Connect. Promo code cũ (giới hạn 100 code mỗi ứng dụng mỗi phiên bản) cấp quyền truy cập miễn phí vào một gói đăng ký. Không giống như offer code, Apple không đưa thông tin giảm giá vào giao dịch promo code — Apple gửi giá đầy đủ của sản phẩm trong biên lai. Kết quả là Adapty ghi lại các giao dịch này theo giá đầy đủ, gây ra sự chênh lệch doanh thu giữa Adapty analytics và App Store Connect. Nếu bạn thấy các giao dịch lịch sử với giá đầy đủ trong khi đáng lẽ phải miễn phí, nhiều khả năng đó là từ promo code cũ. Vì các code này đã bị ngừng hỗ trợ, hãy chuyển sang offer code để theo dõi doanh thu chính xác.
Để hiển thị trang đổi mã trong ứng dụng của bạn: ```swift showLineNumbers Adapty.presentCodeRedemptionSheet() ``` :::danger Qua quan sát của chúng tôi, trang Offer Code Redemption trong một số ứng dụng có thể hoạt động không ổn định. Chúng tôi khuyến nghị chuyển hướng người dùng trực tiếp đến App Store. Để làm điều này, bạn cần mở URL theo định dạng sau: `https://apps.apple.com/redeem?ctx=offercodes&id={apple_app_id}&code={code}` ::: --- # File: restore-purchase --- --- title: "Khôi phục giao dịch mua trong ứng dụng iOS SDK" description: "Tìm hiểu cách khôi phục giao dịch mua trong Adapty để đảm bảo trải nghiệm người dùng liền mạch." --- Khôi phục giao dịch mua là tính năng cho phép người dùng lấy lại quyền truy cập vào nội dung đã mua trước đây, chẳng hạn như các gói đăng ký hoặc in-app purchase, mà không bị tính phí lần nữa. Tính năng này đặc biệt hữu ích cho những người dùng đã gỡ cài đặt và cài đặt lại ứng dụng, hoặc chuyển sang thiết bị mới và muốn truy cập lại nội dung đã mua mà không cần thanh toán thêm. :::note Trong các paywall được xây dựng bằng [Paywall Builder](adapty-paywall-builder), giao dịch mua sẽ được khôi phục tự động mà không cần thêm code từ phía bạn. Nếu đó là trường hợp của bạn — bạn có thể bỏ qua bước này. ::: Để khôi phục giao dịch mua khi bạn không sử dụng [Paywall Builder](adapty-paywall-builder) để tùy chỉnh paywall, hãy gọi phương thức `.restorePurchases()`: ```swift showLineNumbers do { let profile = try await Adapty.restorePurchases() if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // successful access restore } } catch { // handle the error } ``` ```swift showLineNumbers Adapty.restorePurchases { [weak self] result in switch result { case let .success(profile): if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // successful access restore } case let .failure(error): // handle the error } } ``` Tham số phản hồi: | Tham số | Mô tả | |---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Profile** |

Một đối tượng [`AdaptyProfile`](https://swift.adapty.io/documentation/adapty/adaptyprofile). Model này chứa thông tin về mức độ truy cập, các gói đăng ký và các sản phẩm mua một lần.

Kiểm tra **trạng thái mức độ truy cập** để xác định xem người dùng có quyền truy cập vào ứng dụng hay không.

| :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: --- # File: ios-transaction-management --- --- title: "Quản lý giao dịch nâng cao trong iOS SDK" description: "Hoàn tất giao dịch thủ công trong ứng dụng iOS của bạn với Adapty SDK." --- :::note Quản lý giao dịch nâng cao được hỗ trợ trong Adapty iOS SDK bắt đầu từ phiên bản 3.12. ::: Quản lý giao dịch nâng cao trong Adapty cho phép bạn kiểm soát nhiều hơn cách các giao dịch được xử lý, xác minh và hoàn tất. Tính năng này giới thiệu ba tính năng tùy chọn hoạt động cùng nhau: | Tính năng | Mục đích | |-------------------------------------------------------------|----------| | [`appAccountToken`](#assign-appaccounttoken) | Liên kết giao dịch Apple với ID người dùng nội bộ của bạn | | [`jwsTransaction`](#access-the-jws-representation) | Cung cấp payload giao dịch đã ký của Apple để xác thực | | [Hoàn tất thủ công](#control-transaction-finishing-behavior) | Cho phép bạn hoàn tất giao dịch chỉ sau khi backend xác nhận thành công | Kết hợp lại, các công cụ này giúp bạn xây dựng quy trình xác thực tùy chỉnh mạnh mẽ trong khi Adapty vẫn tiếp tục đồng bộ giao dịch với backend của mình. :::important Hầu hết ứng dụng không cần đến tính năng này. Mặc định, Adapty tự động xác thực và hoàn tất các giao dịch StoreKit. Chỉ sử dụng hướng dẫn này nếu bạn chạy xác thực backend riêng hoặc muốn kiểm soát hoàn toàn vòng đời mua hàng. ::: ## Gán `appAccountToken` \{#assign-appaccounttoken\} [`appAccountToken`](https://developer.apple.com/documentation/storekit/product/purchaseoption/appaccounttoken(_:)) là một **UUID** cho phép bạn liên kết các giao dịch App Store với danh tính người dùng nội bộ của mình. StoreKit gắn token này với mọi giao dịch, giúp backend của bạn có thể khớp dữ liệu App Store với người dùng của bạn. Hãy sử dụng một UUID ổn định được tạo cho mỗi người dùng và tái sử dụng nó cho cùng một tài khoản trên các thiết bị. Điều này đảm bảo rằng các giao dịch mua và thông báo App Store được liên kết chính xác. Bạn có thể đặt token theo hai cách – trong quá trình khởi tạo SDK hoặc khi xác định người dùng. :::important Bạn phải luôn truyền `appAccountToken` cùng với `customerUserId`. Nếu chỉ truyền token, nó sẽ không được đưa vào giao dịch. ::: ```swift showLineNumbers // During configuration: let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(customerUserId: "YOUR_USER_ID", withAppAccountToken: UUID()) do { try await Adapty.activate(with: configurationBuilder.build()) } catch { // handle the error } // Or when identifying a user: do { try await Adapty.identify("YOUR_USER_ID", withAppAccountToken: UUID()) } catch { // handle the error } ``` ```swift showLineNumbers // During configuration: let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(customerUserId: "YOUR_USER_ID", withAppAccountToken: ) Adapty.activate(with: configurationBuilder.build()) { error in // handle the error } // Or when identifying a user: Adapty.identify("YOUR_USER_ID", withAppAccountToken: ) { error in if let error { // handle the error } } ``` ## Truy cập biểu diễn JWS \{#access-the-jws-representation\} Khi bạn thực hiện mua hàng, kết quả trả về bao gồm giao dịch của Apple ở [định dạng JWS Compact Serialization](https://developer.apple.com/documentation/storekit/verificationresult/jwsrepresentation-21vgo). Bạn có thể chuyển giá trị này đến backend của mình để xác thực độc lập hoặc ghi log. ```swift let result = try await Adapty.makePurchase(product: paywallProduct) let jwsRepresentation = result.jwsTransaction ``` ## Kiểm soát hành vi hoàn tất giao dịch \{#control-transaction-finishing-behavior\} Mặc định, Adapty tự động hoàn tất các giao dịch StoreKit sau khi xác thực. Nếu bạn cần trì hoãn việc hoàn tất cho đến khi backend xác nhận thành công, hãy đặt hành vi hoàn tất thành thủ công. Trong chế độ này: - Adapty vẫn xác thực giao dịch mua và đồng bộ chúng với backend của mình. - Các giao dịch vẫn chưa được hoàn tất cho đến khi bạn gọi `finish()` một cách tường minh. ```swift var configBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_API_KEY") .with(transactionFinishBehavior: .manual) try await Adapty.activate(with: configBuilder.build()) ``` Khi sử dụng hoàn tất giao dịch thủ công, bạn cần triển khai phương thức delegate `onUnfinishedTransaction` để xử lý các giao dịch chưa hoàn tất: ```swift showLineNumbers title="Swift" extension YourApp: AdaptyDelegate { func onUnfinishedTransaction(_ transaction: AdaptyUnfinishedTransaction) async { // Perform your custom validation logic here // When ready, finish the transaction await transaction.finish() } } ``` Để lấy tất cả các giao dịch chưa hoàn tất hiện tại, hãy sử dụng phương thức `getUnfinishedTransactions()`: ```swift let unfinishedTransactions = try await Adapty.getUnfinishedTransactions() ``` --- # File: implement-observer-mode --- --- title: "Triển khai Observer mode trong iOS SDK" description: "Triển khai observer mode trong Adapty để theo dõi các sự kiện gói đăng ký của người dùng trong iOS SDK." --- Nếu bạn đã có hạ tầng mua hàng riêng và chưa sẵn sàng chuyển hoàn toàn sang Adapty, bạn có thể tìm hiểu về [Observer mode](observer-vs-full-mode). Ở dạng cơ bản, Observer Mode cung cấp analytics nâng cao và tích hợp liền mạch với các hệ thống attribution và analytics. Nếu điều này đáp ứng được nhu cầu của bạn, bạn chỉ cần: 1. Bật tính năng này khi cấu hình Adapty SDK bằng cách đặt tham số `observerMode` thành `true`. 2. [Báo cáo giao dịch](report-transactions-observer-mode) từ hạ tầng mua hàng hiện có của bạn cho Adapty. Nếu bạn cũng cần paywall và A/B test, cần thực hiện thêm một số bước thiết lập như mô tả bên dưới. ## Thiết lập Observer mode \{#observer-mode-setup\} Bật Observer mode nếu bạn tự xử lý việc mua hàng và trạng thái gói đăng ký, đồng thời sử dụng Adapty để gửi các sự kiện gói đăng ký và analytics. :::important Khi chạy ở Observer mode, Adapty SDK sẽ không đóng bất kỳ giao dịch nào, vì vậy hãy đảm bảo bạn tự xử lý điều đó. ::: ```swift showLineNumbers @main struct YourApp: App { init() { // Configure Adapty SDK let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") // Get from Adapty dashboard .with(observerMode: true) let config = configurationBuilder.build() // Activate Adapty SDK asynchronously Task { do { try await Adapty.activate(with: configurationBuilder) } catch { // Handle error appropriately for your app print("Adapty activation failed: ", error) } } var body: some Scene { WindowGroup { // Your content view } } } } ``` ```swift showLineNumbers Task { do { let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "YOUR_PUBLIC_SDK_KEY") // Get from Adapty dashboard .with(observerMode: true) let config = configurationBuilder.build() try await Adapty.activate(with: config) } catch { // Handle error appropriately for your app print("Adapty activation failed: ", error) } } ``` Các tham số: | Tham số | Mô tả | | --------------------------- | ------------------------------------------------------------ | | observerMode | Giá trị boolean kiểm soát [Observer mode](observer-vs-full-mode). Giá trị mặc định là `false`. | ## Sử dụng paywall của Adapty trong Observer Mode \{#using-adapty-paywalls-in-observer-mode\} Nếu bạn cũng muốn sử dụng các tính năng paywall và A/B test của Adapty, bạn hoàn toàn có thể — nhưng cần thực hiện thêm một số thiết lập trong Observer mode. Đây là những việc bạn cần làm ngoài các bước trên: 1. Hiển thị paywall như bình thường đối với [remote config paywalls](present-remote-config-paywalls). Đối với Paywall Builder paywalls, hãy làm theo hướng dẫn thiết lập cụ thể cho [iOS](ios-present-paywall-builder-paywalls-in-observer-mode). 3. [Liên kết paywall](report-transactions-observer-mode) với các giao dịch mua hàng. --- # File: report-transactions-observer-mode --- --- title: "Báo cáo giao dịch trong Observer Mode trên iOS SDK" description: "Báo cáo giao dịch mua hàng trong Adapty Observer Mode để theo dõi thông tin người dùng và doanh thu trên iOS SDK." --- Trong Observer Mode, Adapty SDK không thể tự động theo dõi các giao dịch mua hàng thực hiện qua hệ thống thanh toán hiện có của bạn. Bạn cần báo cáo các giao dịch từ cửa hàng ứng dụng. Điều quan trọng là phải thiết lập điều này **trước** khi phát hành ứng dụng để tránh lỗi trong analytics. Sử dụng `reportTransaction` để báo cáo từng giao dịch một cách tường minh để Adapty nhận diện. :::warning **Đừng bỏ qua việc báo cáo giao dịch!** Nếu bạn không gọi `reportTransaction`, Adapty sẽ không nhận diện được giao dịch, giao dịch đó sẽ không xuất hiện trong analytics và sẽ không được gửi đến các tích hợp. ::: Nếu bạn sử dụng paywall của Adapty, hãy bao gồm `variationId` khi báo cáo giao dịch. Điều này liên kết giao dịch mua hàng với paywall đã kích hoạt nó, đảm bảo analytics paywall chính xác. ```swift showLineNumbers do { // every time when calling transasction.finish() try await Adapty.reportTransaction(transaction, withVariationId: ) } catch { // handle the error } ``` Tham số: | Tham số | Bắt buộc | Mô tả | | --------------- | -------- | ------------------------------------------------------------ | | **transaction** | bắt buộc |
  • Với StoreKit 1: SKPaymentTransaction.
  • Với StoreKit 2: Transaction.
| | **variationId** | tùy chọn | ID duy nhất của biến thể paywall. Lấy từ thuộc tính `variationId` của đối tượng [AdaptyPaywall](https://swift.adapty.io/documentation/adapty/adaptypaywall). |
Trong Observer Mode, Adapty SDK không thể tự động theo dõi các giao dịch mua hàng thực hiện qua hệ thống thanh toán hiện có của bạn. Bạn cần báo cáo các giao dịch từ cửa hàng ứng dụng hoặc khôi phục chúng. Điều quan trọng là phải thiết lập điều này **trước** khi phát hành ứng dụng để tránh lỗi trong analytics. Sử dụng `reportTransaction` để gửi dữ liệu giao dịch đến Adapty. :::warning **Đừng bỏ qua việc báo cáo giao dịch!** Nếu bạn không gọi `reportTransaction`, Adapty sẽ không nhận diện được giao dịch, giao dịch đó sẽ không xuất hiện trong analytics và sẽ không được gửi đến các tích hợp. ::: Nếu bạn sử dụng paywall của Adapty, hãy bao gồm `withVariationId` khi báo cáo giao dịch. Điều này liên kết giao dịch mua hàng với paywall đã kích hoạt nó, đảm bảo analytics paywall chính xác. ```swift showLineNumbers do { // every time when calling transasction.finish() try await Adapty.reportTransaction(transaction, withVariationId: ) } catch { // handle the error } ``` Tham số: | Tham số | Bắt buộc | Mô tả | | --------------- | -------- | ------------------------------------------------------------ | | **transaction** | bắt buộc |
  • Với StoreKit 1: SKPaymentTransaction.
  • Với StoreKit 2: Transaction.
| | **variationId** | tùy chọn | ID duy nhất của biến thể paywall. Lấy từ thuộc tính `variationId` của đối tượng [AdaptyPaywall](https://swift.adapty.io/documentation/adapty/adaptypaywall). |
**Báo cáo giao dịch** - Các phiên bản đến 3.1.x tự động lắng nghe giao dịch trên App Store, nên không cần báo cáo thủ công. - Phiên bản 3.2 không hỗ trợ Observer Mode. **Liên kết paywall với giao dịch** Adapty SDK không thể xác định nguồn gốc của các giao dịch mua hàng vì bạn là người xử lý chúng. Do đó, nếu bạn định sử dụng paywall và/hoặc A/B test trong Observer Mode, bạn cần liên kết giao dịch từ cửa hàng ứng dụng với paywall tương ứng trong code ứng dụng của bạn. Điều này quan trọng cần làm đúng trước khi phát hành ứng dụng, nếu không sẽ dẫn đến lỗi trong analytics. ```swift let variationId = paywall.variationId // There are two overloads: for StoreKit 1 and StoreKit 2 Adapty.setVariationId(variationId, forPurchasedTransaction: transactionId) { error in if error == nil { // successful binding } } ``` Tham số của yêu cầu: | Tham số | Bắt buộc | Mô tả | | ------------- | -------- | ------------------------------------------------------------ | | variationId | bắt buộc | Mã định danh chuỗi của biến thể. Bạn có thể lấy nó bằng thuộc tính `variationId` của đối tượng [AdaptyPaywall](https://swift.adapty.io/documentation/adapty/adaptypaywall). | | transactionId | bắt buộc |

Với StoreKit 1: đối tượng [SKPaymentTransaction](https://developer.apple.com/documentation/storekit/skpaymenttransaction).

Với StoreKit 2: đối tượng [Transaction](https://developer.apple.com/documentation/storekit/transaction).

|
--- # File: ios-troubleshoot-purchases --- --- title: "Khắc phục sự cố mua hàng trong iOS SDK" description: "Khắc phục sự cố mua hàng trong iOS SDK" --- Hướng dẫn này giúp bạn giải quyết các vấn đề thường gặp khi triển khai mua hàng thủ công trong iOS SDK. ## AdaptyError.cantMakePayments trong chế độ observer \{#adaptyelrorcantmakepayments-in-observer-mode\} **Vấn đề**: Bạn nhận được `AdaptyError.cantMakePayments` khi sử dụng `makePurchase` trong chế độ observer. **Nguyên nhân**: Trong chế độ observer, bạn phải tự xử lý việc mua hàng ở phía mình, không nên dùng phương thức `makePurchase` của Adapty. **Giải pháp**: Nếu bạn đang dùng `makePurchase` để thực hiện mua hàng, hãy tắt chế độ observer. Bạn chỉ có thể chọn một trong hai: dùng `makePurchase` hoặc tự xử lý mua hàng trong chế độ observer. Xem [Triển khai chế độ Observer](implement-observer-mode) để biết thêm chi tiết. ## Không tìm thấy makePurchasesCompletionHandlers \{#not-found-makepurchasescompletionhandlers\} **Vấn đề**: Bạn gặp lỗi không tìm thấy `makePurchasesCompletionHandlers`. **Nguyên nhân**: Vấn đề này thường liên quan đến lỗi khi kiểm thử trên sandbox. **Giải pháp**: Tạo một tài khoản sandbox mới và thử lại. Cách này thường giải quyết được các vấn đề liên quan đến purchase completion handler trong sandbox. ## Các vấn đề khác \{#other-issues\} **Vấn đề**: Bạn đang gặp các vấn đề liên quan đến mua hàng khác chưa được đề cập ở trên. **Giải pháp**: Nếu cần, hãy migrate SDK lên phiên bản mới nhất bằng cách sử dụng [hướng dẫn migration](ios-sdk-migration-guides). Nhiều vấn đề đã được khắc phục trong các phiên bản SDK mới hơn. --- # File: ios-web-paywall --- --- title: "Triển khai web paywall trong iOS SDK" description: "Thiết lập web paywall để nhận thanh toán mà không phải chịu phí và kiểm duyệt của App Store." --- :::important Trước khi bắt đầu, hãy đảm bảo bạn đã [cấu hình web paywall trên dashboard](web-paywall) và cài đặt Adapty SDK phiên bản 3.6.1 trở lên. ::: ## Mở web paywall \{#open-web-paywalls\} Nếu bạn đang làm việc với paywall tự phát triển, bạn cần xử lý web paywall bằng phương thức SDK. Phương thức `.openWebPaywall`: 1. Tạo một URL duy nhất cho phép Adapty liên kết paywall cụ thể hiển thị cho người dùng với trang web mà họ được chuyển hướng đến. 2. Theo dõi khi người dùng quay lại ứng dụng, sau đó gọi `.getProfile` theo chu kỳ ngắn để xác định xem quyền truy cập của hồ sơ người dùng có được cập nhật hay không. Nhờ vậy, nếu thanh toán thành công và quyền truy cập được cập nhật, gói đăng ký sẽ được kích hoạt trong ứng dụng gần như ngay lập tức. ```swift showLineNumbers title="Swift" do { try await Adapty.openWebPaywall(for: product) } catch { print("Failed to open web paywall: \(error)") } ``` :::note Có hai phiên bản của phương thức `openWebPaywall`: 1. `openWebPaywall(product)` tạo URL theo paywall và đồng thời thêm dữ liệu sản phẩm vào URL. 2. `openWebPaywall(paywall)` tạo URL theo paywall mà không thêm dữ liệu sản phẩm vào URL. Dùng phiên bản này khi sản phẩm trong Adapty paywall khác với sản phẩm trong web paywall. ::: ## Xử lý lỗi \{#handle-errors\} | Lỗi | Mô tả | Hành động khuyến nghị | |-----------------------------------------|------------------------------------------------------------|--------------------------------------------------------------------------------------| | AdaptyError.paywallWithoutPurchaseUrl | Paywall chưa được cấu hình URL mua hàng qua web | Kiểm tra xem paywall đã được cấu hình đúng trong Adapty Dashboard chưa | | AdaptyError.productWithoutPurchaseUrl | Sản phẩm chưa có URL mua hàng qua web | Xác minh cấu hình sản phẩm trong Adapty Dashboard | | AdaptyError.failedOpeningWebPaywallUrl | Không thể mở URL trong trình duyệt | Kiểm tra cài đặt thiết bị hoặc cung cấp phương thức thanh toán thay thế | | AdaptyError.failedDecodingWebPaywallUrl | Không thể mã hóa đúng các tham số trong URL | Xác minh rằng các tham số URL hợp lệ và được định dạng đúng | ## Ví dụ triển khai \{#implementation-example\} ```swift showLineNumbers title="Swift" class SubscriptionViewController: UIViewController { var paywall: AdaptyPaywall? @IBAction func purchaseButtonTapped(_ sender: UIButton) { guard let paywall = paywall, let product = paywall.products.first else { return } Task { await offerWebPurchase(for: product) } } func offerWebPurchase(for paywallProduct: AdaptyPaywallProduct) async { do { // Attempt to open web paywall try await Adapty.openWebPaywall(for: paywallProduct) } catch let error as AdaptyError { switch error { case .paywallWithoutPurchaseUrl, .productWithoutPurchaseUrl: showAlert(message: "Web purchase is not available for this product.") case .failedOpeningWebPaywallUrl: showAlert(message: "Could not open web browser. Please try again.") default: showAlert(message: "An error occurred: \(error.localizedDescription)") } } catch { showAlert(message: "An unexpected error occurred.") } } // Helper methods private func showAlert(message: String) { /* ... */ } } ``` :::note Sau khi người dùng quay lại ứng dụng, hãy làm mới giao diện để phản ánh các cập nhật hồ sơ người dùng. `AdaptyDelegate` sẽ nhận và xử lý các sự kiện cập nhật hồ sơ người dùng. ::: ## Mở web paywall trong trình duyệt trong ứng dụng \{#open-web-paywalls-in-an-in-app-browser\} :::important Mở web paywall trong trình duyệt trong ứng dụng được hỗ trợ từ Adapty SDK v3.15 trở lên. ::: Mặc định, web paywall sẽ mở trong trình duyệt bên ngoài. Để mang lại trải nghiệm liền mạch cho người dùng, bạn có thể mở web paywall trong trình duyệt tích hợp sẵn trong ứng dụng. Điều này hiển thị trang thanh toán web ngay bên trong ứng dụng của bạn, cho phép người dùng hoàn tất giao dịch mà không cần chuyển sang ứng dụng khác. Để bật tính năng này, đặt tham số `in` thành `.inAppBrowser`: ```swift showLineNumbers title="Swift" do { try await Adapty.openWebPaywall(for: product, in: .inAppBrowser) // default – .externalBrowser } catch { print("Failed to open web paywall: \(error)") } ``` --- # File: identifying-users --- --- title: "Xác định người dùng trong iOS SDK" description: "Xác định người dùng trong Adapty để cải thiện trải nghiệm gói đăng ký được cá nhân hóa." --- Adapty tạo một ID hồ sơ nội bộ cho mỗi người dùng. Tuy nhiên, nếu bạn có hệ thống xác thực riêng, bạn nên đặt Customer User ID của mình. Bạn có thể tìm người dùng theo Customer User ID trong phần [Profiles](profiles-crm) và sử dụng nó trong [server-side API](getting-started-with-server-side-api) — giá trị này sẽ được gửi đến tất cả các tích hợp. ## Đặt customer user ID khi cấu hình \{#set-customer-user-id-on-configuration\} Nếu bạn đã có user ID trong quá trình cấu hình, chỉ cần truyền nó qua tham số `customerUserId` vào phương thức `.activate()`: ```swift showLineNumbers // In your AppDelegate class: let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(customerUserId: "YOUR_USER_ID") do { try await Adapty.activate(with: configurationBuilder.build()) } catch { // handle the error } ``` ```swift showLineNumbers // In your AppDelegate class: let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(customerUserId: "YOUR_USER_ID") Adapty.activate(with: configurationBuilder.build()) { error in // handle the error } ``` :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ## Đặt customer user ID sau khi cấu hình \{#set-customer-user-id-after-configuration\} Nếu bạn chưa có user ID khi cấu hình SDK, bạn có thể đặt nó sau bất kỳ lúc nào bằng phương thức `.identify()`. Trường hợp phổ biến nhất để dùng phương thức này là sau khi đăng ký hoặc đăng nhập, khi người dùng chuyển từ trạng thái ẩn danh sang người dùng đã xác thực. ```swift showLineNumbers do { try await Adapty.identify("YOUR_USER_ID") } catch { // handle the error } ``` ```swift showLineNumbers Adapty.identify("YOUR_USER_ID") { error in if let error { // handle the error } } ``` Tham số yêu cầu: - **Customer User ID** (bắt buộc): chuỗi định danh người dùng. :::warning Gửi lại dữ liệu người dùng quan trọng Trong một số trường hợp, chẳng hạn khi người dùng đăng nhập lại vào tài khoản, máy chủ Adapty đã có thông tin về người dùng đó. Trong những trường hợp này, Adapty SDK sẽ tự động chuyển sang làm việc với người dùng mới. Nếu bạn đã truyền dữ liệu cho người dùng ẩn danh — chẳng hạn như thuộc tính tùy chỉnh hoặc attribution từ các mạng bên thứ ba — bạn cần gửi lại dữ liệu đó cho người dùng đã xác định. Ngoài ra, cần lưu ý rằng bạn nên yêu cầu lại tất cả paywall và sản phẩm sau khi xác định người dùng, vì dữ liệu của người dùng mới có thể khác. ::: ## Đăng xuất và đăng nhập \{#logging-out-and-logging-in\} Bạn có thể đăng xuất người dùng bất kỳ lúc nào bằng cách gọi phương thức `.logout()`: ```swift showLineNumbers do { try await Adapty.logout() } catch { // handle the error } ``` ```swift showLineNumbers Adapty.logout { error in if error == nil { // successful logout } } ``` Sau đó, bạn có thể đăng nhập người dùng bằng phương thức `.identify()`. ## Đặt appAccountToken \{#set-appaccounttoken\} [`appAccountToken`](https://developer.apple.com/documentation/storekit/product/purchaseoption/appaccounttoken(_:)) là một UUID giúp StoreKit 2 của Apple xác định người dùng qua các lần cài đặt ứng dụng và thiết bị khác nhau. Từ Adapty iOS SDK 3.10.2 trở đi, bạn có thể truyền `appAccountToken` khi cấu hình SDK hoặc khi xác định người dùng: ```swift showLineNumbers // During configuration: let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(customerUserId: "YOUR_USER_ID", withAppAccountToken: UUID()) do { try await Adapty.activate(with: configurationBuilder.build()) } catch { // handle the error } // Or when identifying a user: do { try await Adapty.identify("YOUR_USER_ID", withAppAccountToken: UUID()) } catch { // handle the error } ``` ```swift showLineNumbers // During configuration: let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(customerUserId: "YOUR_USER_ID", withAppAccountToken: UUID()) Adapty.activate(with: configurationBuilder.build()) { error in // handle the error } // Or when identifying a user: Adapty.identify("YOUR_USER_ID", withAppAccountToken: UUID()) { error in if let error { // handle the error } } ``` Sau đó, bạn có thể đăng nhập người dùng bằng phương thức `.identify()`. ## Phát hiện người dùng trên nhiều thiết bị \{#detect-users-across-devices\} --- no_index: true --- Khi SDK được kích hoạt, nó tự động đọc các quyền hiện có của người dùng từ StoreKit (iOS) hoặc Google Play Billing (Android) và đồng bộ chúng với backend của Adapty. Một gói đăng ký đang hoạt động sẽ xuất hiện trên hồ sơ người dùng Adapty mà không cần ứng dụng gọi `restorePurchases`. Điều **không** xảy ra tự động là nhận diện rằng một hồ sơ người dùng trên thiết bị mới thuộc về cùng một người dùng như hồ sơ trên thiết bị ban đầu. Adapty khớp các hồ sơ người dùng theo Customer User ID, vì vậy tính liên tục danh tính phụ thuộc vào những gì bạn sử dụng làm CUID. **Những gì Adapty có thể phát hiện trên nhiều thiết bị** | Cài đặt của bạn | Adapty phát hiện được gì | Bạn phải làm gì | | --- | --- | --- | | Customer User ID = `device_id` (không đăng nhập ứng dụng) | Thiết bị mới nhận được CUID khác và do đó có hồ sơ người dùng khác. Gói đăng ký đồng bộ với hồ sơ người dùng mới thông qua sự kiện **Access level updated**, nhưng `subscription_started` không kích hoạt — hồ sơ người dùng mới được coi là người kế thừa của giao dịch mua ban đầu. Các phân tích dựa trên `subscription_started` sẽ đếm thiếu những người dùng quay lại. | Sử dụng ID tài khoản ổn định làm Customer User ID để người dùng quay lại khớp với hồ sơ người dùng hiện có trên các thiết bị. | | Customer User ID = ID tài khoản ổn định (đăng nhập trên mọi thiết bị) | SDK tự động đồng bộ gói đăng ký khi `activate()`, và `identify()` khớp hồ sơ người dùng hiện có theo CUID. | Không cần cài đặt thêm — cả danh tính lẫn gói đăng ký đều được xử lý tự động. | | Người kế thừa Apple Family Sharing | Thành viên gia đình nhận gói đăng ký thông qua sự kiện **Access level updated** — `subscription_started` không kích hoạt. | Lắng nghe sự kiện **Access level updated**. Xem [Apple Family Sharing](apple-family-sharing) để biết ma trận sự kiện đầy đủ. | | Cùng tài khoản Apple/Google, người dùng khác nhau trong ứng dụng | Hồ sơ người dùng đầu tiên ghi lại giao dịch mua trở thành hồ sơ cha. Các hồ sơ người dùng tiếp theo thấy gói đăng ký thông qua chuỗi kế thừa, với một sự kiện **Access level updated**. | Yêu cầu đăng nhập, sau đó chọn [chế độ chia sẻ](sharing-paid-access-between-user-accounts) phù hợp với mô hình của bạn. | **Khôi phục giao dịch mua trên thiết bị mới** Hiển thị nút "Khôi phục giao dịch mua" do người dùng khởi tạo trên paywall của bạn. Apple App Review (hướng dẫn 3.1.1) yêu cầu có nút này, và nó đóng vai trò dự phòng khi quá trình đồng bộ tự động bỏ sót một trường hợp đặc biệt. Nút này nên gọi `restorePurchases` trên SDK của bạn. Không cần gọi `restorePurchases` theo chương trình khi khởi chạy lần đầu trong điều kiện sử dụng bình thường — SDK đã thực hiện tương đương khi `activate()`. Chỉ dùng các lệnh gọi theo chương trình để buộc kiểm tra biên lai mới, ví dụ khi debug trường hợp mất quyền truy cập sau khi `activate()` đã hoàn thành. --- # File: setting-user-attributes --- --- title: "Thiết lập thuộc tính người dùng trong iOS SDK" description: "Tìm hiểu cách thiết lập thuộc tính người dùng trong Adapty để phân khúc đối tượng hiệu quả hơn." --- Bạn có thể thiết lập các thuộc tính tùy chọn như email, số điện thoại, v.v. cho người dùng của ứng dụng. Sau đó, bạn có thể sử dụng các thuộc tính này để tạo [phân khúc](segments) người dùng hoặc chỉ đơn giản là xem chúng trong CRM. ### Thiết lập thuộc tính người dùng \{#setting-user-attributes\} Để thiết lập thuộc tính người dùng, hãy gọi phương thức `.updateProfile()`: ```swift showLineNumbers let builder = AdaptyProfileParameters.Builder() .with(email: "email@email.com") .with(phoneNumber: "+18888888888") .with(firstName: "John") .with(lastName: "Appleseed") .with(gender: .other) .with(birthday: Date()) do { try await Adapty.updateProfile(params: builder.build()) } catch { // handle the error } ``` ```swift showLineNumbers let builder = AdaptyProfileParameters.Builder() .with(email: "email@email.com") .with(phoneNumber: "+18888888888") .with(firstName: "John") .with(lastName: "Appleseed") .with(gender: .other) .with(birthday: Date()) Adapty.updateProfile(params: builder.build()) { error in if error != nil { // handle the error } } ``` Lưu ý rằng các thuộc tính bạn đã thiết lập trước đó bằng phương thức `updateProfile` sẽ không bị xóa. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ### Danh sách các khóa được phép \{#the-allowed-keys-list\} Các khóa `` được phép của `AdaptyProfileParameters.Builder` và các giá trị `` tương ứng được liệt kê bên dưới: | Key | Value | |---|-----| |

email

phoneNumber

firstName

lastName

| String | | gender | Enum, các giá trị được phép là: `female`, `male`, `other` | | birthday | Date | ### Thuộc tính người dùng tùy chỉnh \{#custom-user-attributes\} Bạn có thể thiết lập các thuộc tính tùy chỉnh của riêng mình. Những thuộc tính này thường liên quan đến cách người dùng sử dụng ứng dụng. Ví dụ, với ứng dụng thể dục, chúng có thể là số buổi tập mỗi tuần; với ứng dụng học ngoại ngữ, chúng có thể là trình độ kiến thức của người dùng, v.v. Bạn có thể dùng chúng trong các phân khúc để tạo paywall và ưu đãi có mục tiêu, đồng thời sử dụng trong phân tích để xác định chỉ số sản phẩm nào ảnh hưởng nhiều nhất đến doanh thu. ```swift showLineNumbers do { builder = try builder.with(customAttribute: "value1", forKey: "key1") } catch { // handle key/value validation error } ``` Để xóa khóa hiện có, hãy sử dụng phương thức `.withRemoved(customAttributeForKey:)`: ```swift showLineNumbers do { builder = try builder.withRemoved(customAttributeForKey: "key2") } catch { // handle error } ``` Đôi khi bạn cần biết những thuộc tính tùy chỉnh nào đã được cài đặt trước đó. Để làm điều này, hãy sử dụng trường `customAttributes` của đối tượng `AdaptyProfile`. :::warning Lưu ý rằng giá trị của `customAttributes` có thể không cập nhật kịp thời, vì thuộc tính người dùng có thể được gửi từ nhiều thiết bị khác nhau vào bất kỳ lúc nào, nên các thuộc tính trên máy chủ có thể đã thay đổi sau lần đồng bộ cuối cùng. ::: ### Giới hạn \{#limits\} - Tối đa 30 thuộc tính tùy chỉnh mỗi người dùng - Tên khóa tối đa 30 ký tự. Tên khóa có thể gồm các ký tự chữ và số cùng với các ký tự sau: `_` `-` `.` - Giá trị có thể là chuỗi hoặc số thực (float) với tối đa 50 ký tự. --- # File: subscription-status --- --- title: "Kiểm tra trạng thái gói đăng ký trong iOS SDK" description: "Theo dõi và quản lý trạng thái gói đăng ký của người dùng trong Adapty để cải thiện khả năng giữ chân khách hàng." --- Với Adapty, việc theo dõi trạng thái gói đăng ký trở nên đơn giản hơn bao giờ hết. Bạn không cần phải nhúng thủ công các product ID vào code. Thay vào đó, bạn có thể dễ dàng xác nhận trạng thái gói đăng ký của người dùng bằng cách kiểm tra [mức độ truy cập](access-level) đang hoạt động. Trước khi bắt đầu kiểm tra trạng thái gói đăng ký, hãy thiết lập [App Store Server Notifications](enable-app-store-server-notifications). ## Mức độ truy cập và đối tượng AdaptyProfile \{#access-level-and-the-adaptyprofile-object\} Mức độ truy cập là thuộc tính của đối tượng [AdaptyProfile](https://swift.adapty.io/documentation/adapty/adaptyprofile). Chúng tôi khuyến nghị lấy hồ sơ người dùng khi ứng dụng khởi động, chẳng hạn như khi bạn [xác định người dùng](identifying-users#set-customer-user-id-on-configuration), rồi cập nhật lại mỗi khi có thay đổi. Như vậy, bạn có thể sử dụng đối tượng profile mà không cần phải gọi lại nhiều lần. Để nhận thông báo khi profile được cập nhật, hãy lắng nghe các thay đổi của profile như mô tả trong phần [Lắng nghe cập nhật profile, bao gồm mức độ truy cập](subscription-status#listening-for-subscription-status-updates) bên dưới. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ## Lấy mức độ truy cập từ server \{#retrieving-the-access-level-from-the-server\} Để lấy mức độ truy cập từ server, sử dụng phương thức `.getProfile()`: ```swift showLineNumbers do { let profile = try await Adapty.getProfile() if profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // grant access to premium features } } catch { // handle the error } ``` ```swift showLineNumbers Adapty.getProfile { result in if let profile = try? result.get() { // check the access profile.accessLevels["YOUR_ACCESS_LEVEL"]?.isActive ?? false { // grant access to premium features } } } ``` Tham số trả về: | Tham số | Mô tả | | --------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Profile |

Đối tượng [AdaptyProfile](https://swift.adapty.io/documentation/adapty/adaptyprofile). Thông thường, bạn chỉ cần kiểm tra trạng thái mức độ truy cập của profile để xác định xem người dùng có quyền truy cập premium vào ứng dụng hay không.

Phương thức `.getProfile` luôn cố gắng truy vấn API nên cho kết quả cập nhật nhất. Nếu vì lý do nào đó (ví dụ: không có kết nối internet), Adapty SDK không thể lấy thông tin từ server thì dữ liệu trong cache sẽ được trả về. Cũng cần lưu ý rằng Adapty SDK cập nhật cache của `AdaptyProfile` định kỳ để giữ thông tin luôn được đồng bộ nhất có thể.

| Phương thức `.getProfile()` trả về hồ sơ người dùng, từ đó bạn có thể lấy trạng thái mức độ truy cập. Một ứng dụng có thể có nhiều mức độ truy cập. Ví dụ: nếu bạn có ứng dụng đọc báo và bán gói đăng ký theo từng chủ đề riêng biệt, bạn có thể tạo các mức độ truy cập "sports" và "science". Tuy nhiên, trong hầu hết các trường hợp, bạn chỉ cần một mức độ truy cập — khi đó chỉ cần dùng mức mặc định "premium" là đủ. Dưới đây là ví dụ kiểm tra mức độ truy cập "premium" mặc định: ```swift showLineNumbers do { let profile = try await Adapty.getProfile() let isPremium = profile.accessLevels["premium"]?.isActive ?? false // grant access to premium features } catch { // handle the error } ``` ```swift showLineNumbers Adapty.getProfile { result in if let profile = try? result.get(), profile.accessLevels["premium"]?.isActive ?? false { // grant access to premium features } } ``` ### Lắng nghe cập nhật trạng thái gói đăng ký \{#listening-for-subscription-status-updates\} Mỗi khi gói đăng ký của người dùng thay đổi, Adapty sẽ kích hoạt một sự kiện. Để nhận thông báo từ Adapty, bạn cần thực hiện thêm một số cấu hình: ```swift showLineNumbers Adapty.delegate = self // To receive subscription updates, extend `AdaptyDelegate` with this method: nonisolated func didLoadLatestProfile(_ profile: AdaptyProfile) { // handle any changes to subscription state } ``` Adapty cũng kích hoạt một sự kiện khi ứng dụng khởi động. Trong trường hợp này, trạng thái gói đăng ký được lưu trong cache sẽ được truyền vào. ### Cache trạng thái gói đăng ký \{#subscription-status-cache\} Cache được tích hợp trong Adapty SDK lưu trữ trạng thái gói đăng ký của hồ sơ người dùng. Điều này có nghĩa là ngay cả khi server không khả dụng, dữ liệu trong cache vẫn có thể được truy cập để cung cấp thông tin về trạng thái gói đăng ký. Tuy nhiên, cần lưu ý rằng không thể truy vấn dữ liệu trực tiếp từ cache. SDK định kỳ gửi yêu cầu đến server mỗi phút một lần để kiểm tra xem có cập nhật hay thay đổi nào liên quan đến profile không. Nếu có bất kỳ thay đổi nào — chẳng hạn như giao dịch mới hay các cập nhật khác — chúng sẽ được đồng bộ vào dữ liệu cache để giữ thông tin luôn nhất quán với server. --- # File: ios-deal-with-att --- --- title: "Xử lý ATT trong iOS SDK" description: "Bắt đầu với Adapty trên iOS để đơn giản hóa việc thiết lập và quản lý gói đăng ký." --- Nếu ứng dụng của bạn sử dụng framework AppTrackingTransparency và hiển thị yêu cầu xác nhận quyền theo dõi ứng dụng cho người dùng, bạn cần gửi [trạng thái ủy quyền](https://developer.apple.com/documentation/apptrackingtransparency/attrackingmanager/authorizationstatus/) đến Adapty. ```swift showLineNumbers let builder = AdaptyProfileParameters.Builder() .with(appTrackingTransparencyStatus: .authorized) do { try await Adapty.updateProfile(params: builder.build()) } catch { // handle the error } ``` ```swift showLineNumbers if #available(iOS 14, macOS 11.0, *) { let builder = AdaptyProfileParameters.Builder() .with(appTrackingTransparencyStatus: .authorized) Adapty.updateProfile(params: builder.build()) { [weak self] error in if error != nil { // handle the error } } } ``` :::warning Chúng tôi khuyến nghị bạn gửi giá trị này càng sớm càng tốt ngay khi nó thay đổi — chỉ như vậy dữ liệu mới được gửi kịp thời đến các tích hợp bạn đã cấu hình. ::: --- # File: kids-mode --- --- title: "Chế độ Kids Mode trong iOS SDK" description: "Dễ dàng bật Kids Mode để tuân thủ chính sách của Apple. Không thu thập IDFA hay dữ liệu quảng cáo trong iOS SDK." --- Nếu ứng dụng iOS của bạn dành cho trẻ em, bạn phải tuân thủ các chính sách của [Apple](https://developer.apple.com/kids/). Nếu đang sử dụng Adapty SDK, bạn chỉ cần thực hiện vài bước đơn giản để cấu hình SDK theo đúng các chính sách này và vượt qua quy trình kiểm duyệt của cửa hàng. ## Yêu cầu cần thực hiện \{#whats-required\} Bạn cần cấu hình Adapty SDK để tắt việc thu thập: - [IDFA (Identifier for Advertisers)](https://en.wikipedia.org/wiki/Identifier_for_Advertisers) - [Địa chỉ IP](https://www.ftc.gov/system/files/ftc_gov/pdf/p235402_coppa_application.pdf) Ngoài ra, chúng tôi khuyến nghị sử dụng customer user ID một cách cẩn thận. ID người dùng có định dạng `` chắc chắn sẽ bị xem là thu thập dữ liệu cá nhân, cũng như việc dùng email. Trong Kids Mode, cách tốt nhất là sử dụng các định danh ngẫu nhiên hoặc ẩn danh (ví dụ: hashed ID hoặc UUID do thiết bị tạo ra) để đảm bảo tuân thủ. ## Bật Kids Mode \{#enabling-kids-mode\} ### Cập nhật trong Adapty Dashboard \{#updates-in-the-adapty-dashboard\} Trong Adapty Dashboard, bạn cần tắt tính năng thu thập địa chỉ IP. Để thực hiện, vào [App settings](https://app.adapty.io/settings/general) và nhấn **Disable IP address collection** trong phần **Collect users' IP address**. ### Cập nhật trong code của ứng dụng \{#updates-in-your-mobile-app-code\} Để tuân thủ các chính sách, hãy tắt việc thu thập IDFA và địa chỉ IP của người dùng. Nếu bạn dùng Swift Package Manager, bạn có thể bật Kids Mode bằng cách chọn module **Adapty_KidsMode** trong Xcode khi cài đặt SDK. Trong Xcode, vào **File** -> **Add Package Dependency...**. Lưu ý rằng các bước thêm package dependency có thể khác nhau giữa các phiên bản Xcode, hãy tham khảo tài liệu Xcode nếu cần. 1. Nhập URL repository: ``` https://github.com/adaptyteam/AdaptySDK-iOS.git ``` 2. Chọn phiên bản (khuyến nghị dùng phiên bản ổn định mới nhất) và nhấn **Add Package**. 3. Trong cửa sổ **Choose Package Products**, chọn các module bạn cần: - **Adapty_KidsMode** (module cốt lõi) - **AdaptyUI_KidsMode** (tùy chọn - chỉ cần nếu bạn dự định dùng Paywall Builder) Bạn không cần thêm bất kỳ package nào khác. 4. Nhấn **Add Package** để hoàn tất cài đặt. 5. Trong code của bạn, viết `import Adapty_KidsMode` thay vì `import Adapty`, và `import AdaptyUI_KidsMode` thay vì `import AdaptyUI`: ```swift ``` 1. Cập nhật Podfile của bạn: - Nếu bạn **chưa có** phần `post_install`, hãy thêm toàn bộ đoạn code bên dưới. - Nếu bạn **đã có** phần `post_install`, hãy hợp nhất các dòng được đánh dấu vào đó. ```ruby showLineNumbers title="Podfile" def adapty_enable_kids_mode(installer) installer.pods_project.targets.each do |target| next unless target.name == 'Adapty' target.build_configurations.each do |config| flags = config.build_settings['OTHER_SWIFT_FLAGS'] || '$(inherited)' flags = flags.join(' ') if flags.is_a?(Array) config.build_settings['OTHER_SWIFT_FLAGS'] = "#{flags} -DADAPTY_KIDS_MODE" end target.frameworks_build_phase.files.dup.each do |bf| target.frameworks_build_phase.remove_build_file(bf) if bf.display_name.to_s.include?('AdSupport') end end installer.pods_project.save Dir.glob(File.join(installer.sandbox.root, 'Target Support Files', '**', '*.xcconfig')).each do |xc| File.write(xc, File.read(xc).gsub(/\s*-framework\s+"?AdSupport"?/, '')) end end post_install do |installer| # ... keep your existing post_install body (Flutter adds one automatically) ... adapty_enable_kids_mode(installer) # <-- enable Adapty Kids Mode end ``` 2. Chạy lệnh sau để áp dụng các thay đổi: ```sh showLineNumbers title="Shell" pod install ``` --- # File: get-onboardings --- --- title: "Lấy onboarding và cấu hình của chúng" description: "Tìm hiểu cách lấy onboarding trong Adapty." --- :::tip **Bắt đầu từ SDK v4 (beta)**, bạn có thể xây dựng [flow](get-pb-paywalls) như một giải pháp mạnh mẽ hơn so với onboarding. Khác với onboarding chạy bên trong WebView, flow render trực tiếp trên thiết bị — mang lại animation mượt mà hơn, giao diện iOS nhất quán, tốc độ tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flow & paywall](get-pb-paywalls) và [Hiển thị flow & paywall](ios-present-paywalls) để bắt đầu. ::: Sau khi [bạn đã thiết kế phần giao diện cho onboarding](design-onboarding) bằng builder trong Adapty Dashboard, bạn có thể hiển thị nó trong ứng dụng di động của mình. Bước đầu tiên trong quá trình này là lấy onboarding được liên kết với placement cùng cấu hình hiển thị của nó như mô tả bên dưới. Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã cài đặt [Adapty iOS, Android, React Native hoặc Flutter SDK](installation-of-adapty-sdks) phiên bản 3.8.0 trở lên. 2. Bạn đã [tạo một onboarding](create-onboarding). 3. Bạn đã thêm onboarding vào một [placement](placements). ## Lấy onboarding \{#fetch-onboarding\} Khi bạn tạo một [onboarding](onboardings) bằng builder no-code của chúng tôi, nó được lưu trữ dưới dạng một container với cấu hình mà ứng dụng của bạn cần lấy và hiển thị. Container này quản lý toàn bộ trải nghiệm — nội dung nào xuất hiện, cách trình bày, và cách xử lý các tương tác của người dùng (như câu trả lời quiz hoặc dữ liệu nhập form). Container cũng tự động theo dõi các sự kiện analytics, vì vậy bạn không cần triển khai tính năng theo dõi lượt xem riêng. Để đạt hiệu suất tốt nhất, hãy lấy cấu hình onboarding sớm để ảnh có đủ thời gian tải xuống trước khi hiển thị cho người dùng. Để lấy một onboarding, sử dụng phương thức `getOnboarding`: ```swift showLineNumbers do { let onboarding = try await Adapty.getOnboarding(placementId: "YOUR_PLACEMENT_ID") // the requested onboarding } catch { // handle the error } ``` Tham số: | Tham số | Bắt buộc | Mô tả | |---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | bắt buộc | Định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** |

tùy chọn

mặc định: `en`

|

Định danh của bản dịch onboarding. Tham số này được kỳ vọng là một mã ngôn ngữ gồm một hoặc hai subtag được phân tách bằng dấu trừ (**-**). Subtag đầu tiên là ngôn ngữ, subtag thứ hai là vùng.

Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha Brazil.

Xem [Bản địa hóa và mã locale](localizations-and-locale-codes) để biết thêm thông tin về mã locale và cách chúng tôi khuyến nghị sử dụng chúng.

| | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` |

Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu cache trong trường hợp thất bại. Chúng tôi khuyến nghị tùy chọn này vì nó đảm bảo người dùng của bạn luôn nhận được dữ liệu mới nhất.

Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp kết nối internet không ổn định, hãy cân nhắc sử dụng `.returnCacheDataElseLoad` để trả về dữ liệu cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng họ sẽ trải nghiệm thời gian tải nhanh hơn, bất kể kết nối internet của họ yếu đến mức nào. Cache được cập nhật thường xuyên, vì vậy an toàn khi sử dụng trong phiên làm việc để tránh các yêu cầu mạng.

Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi ứng dụng được cài đặt lại hoặc thông qua việc dọn dẹp thủ công.

Adapty SDK lưu trữ onboarding cục bộ theo hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và onboarding dự phòng. Chúng tôi cũng sử dụng CDN để lấy onboarding nhanh hơn và một máy chủ dự phòng độc lập trong trường hợp CDN không khả dụng. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản mới nhất của onboarding trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet kém.

| | **loadTimeout** | mặc định: 5 giây |

Giá trị này giới hạn timeout cho phương thức này. Nếu timeout được đạt tới, dữ liệu cache hoặc fallback cục bộ sẽ được trả về.

Lưu ý rằng trong một số trường hợp hiếm gặp, phương thức này có thể timeout muộn hơn một chút so với giá trị được chỉ định trong `loadTimeout`, vì thao tác có thể bao gồm nhiều yêu cầu khác nhau bên dưới.

| Tham số phản hồi: | Tham số | Mô tả | |:----------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------| | Onboarding | Một đối tượng [`AdaptyOnboarding`](https://swift.adapty.io/documentation/adapty/adaptyonboarding) với: định danh và cấu hình onboarding, Remote Config, và một số thuộc tính khác. | ## Tăng tốc lấy onboarding với onboarding đối tượng mặc định \{#speed-up-onboarding-fetching-with-default-audience-onboarding\} Thông thường, onboarding được lấy gần như ngay lập tức, vì vậy bạn không cần lo lắng về việc tăng tốc quá trình này. Tuy nhiên, trong trường hợp bạn có nhiều đối tượng và onboarding, và người dùng của bạn có kết nối internet yếu, việc lấy onboarding có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị một onboarding mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị onboarding nào. Để giải quyết vấn đề này, bạn có thể sử dụng phương thức `getOnboardingForDefaultAudience`, phương thức này lấy onboarding của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị là lấy onboarding bằng phương thức `getOnboarding`, như được mô tả chi tiết trong phần [Lấy onboarding](#fetch-onboarding) ở trên. :::warning Hãy cân nhắc sử dụng `getOnboarding` thay vì `getOnboardingForDefaultAudience`, vì phương thức sau có những hạn chế quan trọng: - **Vấn đề tương thích**: Có thể gây ra sự cố khi hỗ trợ nhiều phiên bản ứng dụng, đòi hỏi phải thiết kế tương thích ngược hoặc chấp nhận rằng các phiên bản cũ hơn có thể hiển thị không chính xác. - **Không có cá nhân hóa**: Chỉ hiển thị nội dung cho đối tượng "All Users", loại bỏ khả năng nhắm mục tiêu theo quốc gia, attribution hoặc thuộc tính tùy chỉnh. Nếu tốc độ lấy nhanh hơn vượt trội hơn những hạn chế này với trường hợp sử dụng của bạn, hãy dùng `getOnboardingForDefaultAudience` như hiển thị bên dưới. Nếu không, hãy sử dụng `getOnboarding` như được mô tả [ở trên](#fetch-onboarding). ::: ```swift showLineNumbers Adapty.getOnboardingForDefaultAudience(placementId: "YOUR_PLACEMENT_ID") { result in switch result { case let .success(onboarding): // the requested onboarding case let .failure(error): // handle the error } } ``` Tham số: | Tham số | Bắt buộc | Mô tả | |---------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **placementId** | bắt buộc | Định danh của [Placement](placements) mong muốn. Đây là giá trị bạn đã chỉ định khi tạo placement trong Adapty Dashboard. | | **locale** |

tùy chọn

mặc định: `en`

|

Định danh của bản dịch onboarding. Tham số này được kỳ vọng là một mã ngôn ngữ gồm một hoặc hai subtag được phân tách bằng dấu trừ (**-**). Subtag đầu tiên là ngôn ngữ, subtag thứ hai là vùng.

Ví dụ: `en` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha Brazil.

Xem [Bản địa hóa và mã locale](localizations-and-locale-codes) để biết thêm thông tin về mã locale và cách chúng tôi khuyến nghị sử dụng chúng.

| | **fetchPolicy** | mặc định: `.reloadRevalidatingCacheData` |

Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu cache trong trường hợp thất bại. Chúng tôi khuyến nghị tùy chọn này vì nó đảm bảo người dùng của bạn luôn nhận được dữ liệu mới nhất.

Tuy nhiên, nếu bạn cho rằng người dùng của mình thường xuyên gặp kết nối internet không ổn định, hãy cân nhắc sử dụng `.returnCacheDataElseLoad` để trả về dữ liệu cache nếu có. Trong trường hợp này, người dùng có thể không nhận được dữ liệu mới nhất tuyệt đối, nhưng họ sẽ trải nghiệm thời gian tải nhanh hơn, bất kể kết nối internet của họ yếu đến mức nào. Cache được cập nhật thường xuyên, vì vậy an toàn khi sử dụng trong phiên làm việc để tránh các yêu cầu mạng.

Lưu ý rằng cache vẫn được giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi ứng dụng được cài đặt lại hoặc thông qua việc dọn dẹp thủ công.

Adapty SDK lưu trữ onboarding cục bộ theo hai lớp: cache được cập nhật thường xuyên như mô tả ở trên và onboarding dự phòng. Chúng tôi cũng sử dụng CDN để lấy onboarding nhanh hơn và một máy chủ dự phòng độc lập trong trường hợp CDN không khả dụng. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản mới nhất của onboarding trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet kém.

| --- # File: ios-present-onboardings --- --- title: "Hiển thị onboarding trong iOS SDK" description: "Khám phá cách hiển thị onboarding trên iOS để tăng tỷ lệ chuyển đổi và doanh thu." --- :::tip **Từ SDK v4 (beta)**, bạn có thể xây dựng [flow](get-pb-paywalls) như một giải pháp mạnh mẽ hơn so với onboarding. Khác với onboarding chạy bên trong WebView, flow render trực tiếp trên thiết bị — mang lại animation mượt mà hơn, giao diện iOS nhất quán, tốc độ tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flow & paywall](get-pb-paywalls) và [Hiển thị flow & paywall](ios-present-paywalls) để bắt đầu. ::: Nếu bạn đã tùy chỉnh onboarding bằng builder, bạn không cần lo lắng về việc render nó trong code ứng dụng để hiển thị cho người dùng. Onboarding đó đã bao gồm cả nội dung cần hiển thị lẫn cách hiển thị. Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã cài đặt [Adapty iOS SDK](sdk-installation-ios) phiên bản 3.8.0 trở lên. 2. Bạn đã [tạo một onboarding](create-onboarding). 3. Bạn đã thêm onboarding vào một [placement](placements). ## Hiển thị onboarding bằng Swift \{#present-onboardings-in-swift\} Để hiển thị onboarding trực quan trên màn hình thiết bị, hãy thực hiện các bước sau: 1. Lấy cấu hình view onboarding bằng phương thức `.getOnboardingConfiguration`. 2. Khởi tạo onboarding trực quan mà bạn muốn hiển thị bằng phương thức `.onboardingController`: Tham số đầu vào: | Tham số | Bắt buộc | Mô tả | |:------------------------------|:---------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------| | **onboarding configuration** | bắt buộc | Một đối tượng `AdaptyUI.OnboardingConfiguration` chứa tất cả các thuộc tính của onboarding. Dùng phương thức `AdaptyUI.getOnboardingConfiguration` để lấy nó. | | **delegate** | bắt buộc | Một `AdaptyOnboardingControllerDelegate` để lắng nghe các sự kiện của onboarding. | Kết quả trả về: | Đối tượng | Mô tả | |:-------------------------------|:--------------------------------------------------------| | **AdaptyOnboardingController** | Một đối tượng đại diện cho màn hình onboarding được yêu cầu | 3. Sau khi đối tượng được tạo thành công, bạn có thể hiển thị nó trên màn hình thiết bị: ```swift showLineNumbers title="Swift" import Adapty import AdaptyUI // 0. Get an onboarding if you haven't done it yet let onboarding = try await Adapty.getOnboarding(placementId: "YOUR_PLACEMENT_ID") // 1. Obtain the onboarding view configuration: let configuration = try AdaptyUI.getOnboardingConfiguration(forOnboarding: onboarding) // 2. Create Onboarding View Controller let onboardingController = try AdaptyUI.onboardingController( with: configuration, delegate: ) // 3. Present it to the user present(onboardingController, animated: true) ``` ## Hiển thị onboarding bằng SwiftUI \{#present-onboardings-in-swiftui\} Để hiển thị onboarding trực quan trên màn hình thiết bị trong SwiftUI: ```swift showLineNumbers title="SwiftUI" // 1. Obtain the onboarding view configuration: let configuration = try AdaptyUI.getOnboardingConfiguration(forOnboarding: onboarding) // 2. Display the Onboarding View within your view hierarchy AdaptyOnboardingView( configuration: configuration, placeholder: { Text("Your Placeholder View") }, onCloseAction: { action in // hide the onboarding view }, onError: { error in // handle the error } ) ``` ## Thêm hiệu ứng chuyển cảnh mượt mà giữa màn hình splash và onboarding \{#add-smooth-transitions-between-the-splash-screen-and-onboarding\} Mặc định, giữa màn hình splash và onboarding, bạn sẽ thấy màn hình tải cho đến khi onboarding được load xong. Tuy nhiên, nếu muốn quá trình chuyển cảnh mượt mà hơn, bạn có thể tùy chỉnh — kéo dài màn hình splash hoặc hiển thị nội dung khác trong thời gian chờ. Để làm điều này, hãy định nghĩa một placeholder (nội dung sẽ hiển thị trong khi onboarding đang được tải). Nếu bạn định nghĩa placeholder, onboarding sẽ được tải ngầm và tự động hiển thị khi sẵn sàng. ```swift showLineNumbers extension YourOnboardingManagerClass: AdaptyOnboardingControllerDelegate { func onboardingsControllerLoadingPlaceholder( _ controller: AdaptyOnboardingController ) -> UIView? { // instantiate and return the UIView which will be presented while onboarding is being loaded } } ``` ```swift showLineNumbers AdaptyOnboardingView( configuration: configuration, placeholder: { // define your placeholder view, which will be presented while onboarding is being loaded }, // the rest of the implementation ) ``` ## Tùy chỉnh cách mở link trong onboarding \{#customize-how-links-open-in-onboardings\} :::important Tùy chỉnh cách mở link trong onboarding được hỗ trợ từ Adapty SDK v3.15.1 trở lên. ::: Mặc định, các link trong onboarding sẽ mở trong trình duyệt trong ứng dụng. Điều này mang lại trải nghiệm liền mạch bằng cách hiển thị trang web ngay trong ứng dụng, giúp người dùng xem nội dung mà không cần chuyển sang ứng dụng khác. Nếu bạn muốn mở link bằng trình duyệt ngoài thay thế, bạn có thể tùy chỉnh hành vi này bằng cách đặt tham số `externalUrlsPresentation` thành `.externalBrowser`: ```swift showLineNumbers let configuration = try AdaptyUI.getOnboardingConfiguration( forOnboarding: onboarding, externalUrlsPresentation: .externalBrowser // default – .inAppBrowser ) ``` --- # File: ios-handling-onboarding-events --- --- title: "Xử lý sự kiện onboarding trong iOS SDK" description: "Xử lý các sự kiện liên quan đến onboarding trong iOS bằng Adapty." --- :::tip **Từ SDK v4 (beta)**, bạn có thể xây dựng [flow](get-pb-paywalls) như một lựa chọn mạnh mẽ hơn so với onboarding. Không giống như onboarding chạy bên trong WebView, flow render trực tiếp trên thiết bị — mang lại animation mượt mà hơn, giao diện iOS nhất quán, tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flow & paywall](get-pb-paywalls) và [Hiển thị flow & paywall](ios-present-paywalls) để bắt đầu. ::: Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã cài đặt [Adapty iOS SDK](sdk-installation-ios) phiên bản 3.8.0 trở lên. 2. Bạn đã [tạo một onboarding](create-onboarding). 3. Bạn đã thêm onboarding vào một [placement](placements). Các onboarding được cấu hình bằng builder sẽ tạo ra các sự kiện mà ứng dụng của bạn có thể xử lý. Hãy tìm hiểu cách xử lý các sự kiện này bên dưới. Để kiểm soát hoặc theo dõi các tiến trình xảy ra trên màn hình onboarding trong ứng dụng di động của bạn, hãy triển khai các phương thức `AdaptyOnboardingControllerDelegate`. ## Hành động tùy chỉnh \{#custom-actions\} Trong builder, bạn có thể thêm hành động **custom** cho một nút và gán cho nó một ID. Sau đó, bạn có thể sử dụng ID này trong code và xử lý nó như một hành động tùy chỉnh. Ví dụ: khi người dùng nhấn vào một nút tùy chỉnh như **Login** hoặc **Allow notifications**, phương thức delegate `onboardingController` sẽ được kích hoạt với case `.custom(id:)` và tham số `actionId` chính là **Action ID** từ builder. Bạn có thể tự tạo ID của riêng mình, chẳng hạn như "allowNotifications". ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, onCustomAction action: AdaptyOnboardingsCustomAction) { if action.actionId == "allowNotifications" { // Request notification permissions } } func onboardingController(_ controller: AdaptyOnboardingController, didFailWithError error: AdaptyUIError) { // Handle errors } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```json { "actionId": "allowNotifications", "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "screensTotal": 3 } } ```
## Đóng onboarding \{#closing-onboarding\} Onboarding được coi là đã đóng khi người dùng nhấn vào một nút có hành động **Close** được gán. :::important Lưu ý rằng bạn cần tự xử lý những gì xảy ra khi người dùng đóng onboarding. Ví dụ: bạn cần dừng hiển thị onboarding đó. ::: Ví dụ: ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, onCloseAction action: AdaptyOnboardingsCloseAction) { controller.dismiss(animated: true) } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```json { "action_id": "close_button", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "final_screen", "screen_index": 3, "total_screens": 4 } } ```
## Mở paywall \{#opening-a-paywall\} :::tip Xử lý sự kiện này để mở paywall nếu bạn muốn mở nó bên trong onboarding. Nếu bạn muốn mở paywall sau khi đóng onboarding, có một cách đơn giản hơn — xử lý [`AdaptyOnboardingsCloseAction`](#closing-onboarding) và mở paywall mà không cần dựa vào dữ liệu sự kiện. ::: Cách liền mạch nhất để làm việc với paywall trong onboarding là đặt action ID bằng với placement ID của paywall. Như vậy, sau sự kiện `AdaptyOnboardingsOpenPaywallAction`, bạn có thể dùng placement ID để lấy và mở paywall ngay lập tức. Lưu ý rằng tại một thời điểm chỉ có thể hiển thị một view (paywall hoặc onboarding) trên màn hình. Nếu bạn hiển thị paywall chồng lên onboarding, bạn không thể lập trình điều khiển onboarding ở nền. Việc cố gắng dismiss onboarding sẽ đóng paywall thay vào đó, khiến onboarding vẫn hiển thị. Để tránh điều này, hãy luôn dismiss view onboarding trước khi hiển thị paywall. ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, onPaywallAction action: AdaptyOnboardingsOpenPaywallAction) { // Dismiss onboarding before presenting the flow controller.dismiss(animated: true) { Task { do { // Get the flow using the placement ID from the action let flow = try await Adapty.getFlow(placementId: action.actionId) // Get the flow configuration let flowConfiguration = try await AdaptyUI.getFlowConfiguration( forFlow: flow ) // Create and present the flow controller let flowController = try AdaptyUI.flowController( with: flowConfiguration, delegate: self ) // Present the flow from the root view controller if let rootVC = UIApplication.shared.windows.first?.rootViewController { rootVC.present(flowController, animated: true) } } catch { // Handle any errors that occur during flow loading print("Failed to present flow: \(error)") } } } } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```json { "action_id": "premium_offer_1", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "pricing_screen", "screen_index": 2, "total_screens": 4 } } ```
## Hoàn tất tải onboarding \{#finishing-loading-onboarding\} Khi onboarding hoàn tất tải, phương thức này sẽ được gọi: ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, didFinishLoading action: OnboardingsDidFinishLoadingAction) { // Handle loading completion } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```json { "meta": { "onboarding_id": "onboarding_123", "screen_cid": "welcome_screen", "screen_index": 0, "total_screens": 4 } } ```
## Theo dõi điều hướng \{#tracking-navigation\} Phương thức `onAnalyticsEvent` được gọi khi các sự kiện analytics khác nhau xảy ra trong quá trình onboarding. Đối tượng `event` có thể là một trong các loại sau: |Loại | Mô tả | |------------|-------------| | `onboardingStarted` | Khi onboarding đã được tải xong | | `screenPresented` | Khi bất kỳ màn hình nào được hiển thị | | `screenCompleted` | Khi một màn hình hoàn tất. Bao gồm `elementId` tùy chọn (định danh của phần tử đã hoàn thành) và `reply` tùy chọn (phản hồi từ người dùng). Được kích hoạt khi người dùng thực hiện bất kỳ hành động nào để thoát khỏi màn hình. | | `secondScreenPresented` | Khi màn hình thứ hai được hiển thị | | `userEmailCollected` | Được kích hoạt khi email của người dùng được thu thập qua trường nhập liệu | | `onboardingCompleted` | Được kích hoạt khi người dùng đến màn hình có ID `final`. Nếu bạn cần sự kiện này, hãy [gán ID `final` cho màn hình cuối cùng](design-onboarding). | | `unknown` | Cho bất kỳ loại sự kiện không nhận dạng được. Bao gồm `name` (tên của sự kiện không xác định) và `meta` (metadata bổ sung) | Mỗi sự kiện bao gồm thông tin `meta` chứa: | Trường | Mô tả | |------------|-------------| | `onboardingId` | Định danh duy nhất của onboarding flow | | `screenClientId` | Định danh của màn hình hiện tại | | `screenIndex` | Vị trí của màn hình hiện tại trong flow | | `screensTotal` | Tổng số màn hình trong flow | Đây là ví dụ về cách bạn có thể sử dụng các sự kiện analytics để theo dõi: ```swift func onboardingController(_ controller: AdaptyOnboardingController, onAnalyticsEvent event: AdaptyOnboardingsAnalyticsEvent) { switch event { case .onboardingStarted(let meta): // Track onboarding start trackEvent("onboarding_started", meta: meta) case .screenPresented(let meta): // Track screen presentation trackEvent("screen_presented", meta: meta) case .screenCompleted(let meta, let elementId, let reply): // Track screen completion with user response trackEvent("screen_completed", meta: meta, elementId: elementId, reply: reply) case .onboardingCompleted(let meta): // Track successful onboarding completion trackEvent("onboarding_completed", meta: meta) case .unknown(let meta, let name): // Handle unknown events trackEvent(name, meta: meta) // Handle other cases as needed } } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```javascript // onboardingStarted { "name": "onboarding_started", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "welcome_screen", "screen_index": 0, "total_screens": 4 } } // screenPresented { "name": "screen_presented", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "interests_screen", "screen_index": 2, "total_screens": 4 } } // screenCompleted { "name": "screen_completed", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 1, "total_screens": 4 }, "params": { "element_id": "profile_form", "reply": "success" } } // secondScreenPresented { "name": "second_screen_presented", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 1, "total_screens": 4 } } // userEmailCollected { "name": "user_email_collected", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 1, "total_screens": 4 } } // onboardingCompleted { "name": "onboarding_completed", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "final_screen", "screen_index": 3, "total_screens": 4 } } ```
--- # File: ios-onboarding-input --- --- title: "Xử lý dữ liệu từ onboarding trong iOS SDK" description: "Lưu và sử dụng dữ liệu từ onboarding trong ứng dụng iOS của bạn với Adapty SDK." --- :::tip **Bắt đầu từ SDK v4 (beta)**, bạn có thể xây dựng [flow](get-pb-paywalls) như một lựa chọn mạnh mẽ hơn so với onboarding. Khác với onboarding chạy trong WebView, flow render trực tiếp trên thiết bị — mang lại hiệu ứng chuyển động mượt mà hơn, giao diện iOS nhất quán, tốc độ tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flow & paywall](get-pb-paywalls) và [Hiển thị flow & paywall](ios-present-paywalls) để bắt đầu. ::: Khi người dùng trả lời câu hỏi trong quiz hoặc nhập dữ liệu vào trường nhập liệu, phương thức `onStateUpdatedAction` sẽ được gọi. Bạn có thể lưu hoặc xử lý loại trường đó trong code của mình. Ví dụ: ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, onStateUpdatedAction action: AdaptyOnboardingsStateUpdatedAction) { // Store user preferences or responses switch action.params { case .select(let params): // Handle single selection case .multiSelect(let params): // Handle multiple selections case .input(let params): // Handle text input case .datePicker(let params): // Handle date selection } } ``` Đối tượng `action` chứa: | Tham số | Mô tả | |----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `elementId` | Định danh duy nhất của phần tử nhập liệu. Bạn có thể dùng nó để liên kết câu hỏi với câu trả lời khi lưu dữ liệu. | | `params` | Đối tượng dữ liệu đầu vào của người dùng, chứa các thuộc tính type và value. | | `params.type` | Loại phần tử nhập liệu. Có thể là:
• `"select"` - Chọn một trong các tùy chọn
• `"multiSelect"` - Chọn nhiều trong các tùy chọn
• `"input"` - Trường nhập văn bản
• `"datePicker"` - Chọn ngày tháng | | `params.value` | Giá trị người dùng đã chọn hoặc nhập. Cấu trúc tùy theo loại:
• `select`: Object gồm `id`, `value`, `label`
• `multiSelect`: Mảng các object gồm `id`, `value`, `label`
• `input`: Object gồm `type`, `value`
• `datePicker`: Object gồm `day`, `month`, `year` |
Ví dụ về dữ liệu đã lưu (có thể khác trong cài đặt của bạn) ```javascript // Example of a saved select action { "elementId": "preference_selector", "meta": { "onboardingId": "onboarding_123", "screenClientId": "preferences_screen", "screenIndex": 1, "screensTotal": 3 }, "params": { "type": "select", "value": { "id": "option_1", "value": "premium", "label": "Premium Plan" } } } // Example of a saved multi-select action { "elementId": "interests_selector", "meta": { "onboardingId": "onboarding_123", "screenClientId": "interests_screen", "screenIndex": 2, "screensTotal": 3 }, "params": { "type": "multiSelect", "value": [ { "id": "interest_1", "value": "sports", "label": "Sports" }, { "id": "interest_2", "value": "music", "label": "Music" } ] } } // Example of a saved input action { "elementId": "name_input", "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "screensTotal": 3 }, "params": { "type": "input", "value": { "type": "text", "value": "John Doe" } } } // Example of a saved date picker action { "elementId": "birthday_picker", "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "screensTotal": 3 }, "params": { "type": "datePicker", "value": { "day": 15, "month": 6, "year": 1990 } } } ```
## Các trường hợp sử dụng \{#use-cases\} ### Bổ sung dữ liệu vào hồ sơ người dùng \{#enrich-user-profiles-with-data\} Nếu bạn muốn liên kết ngay dữ liệu đầu vào với hồ sơ người dùng và tránh hỏi họ lặp lại cùng một thông tin, bạn cần [cập nhật hồ sơ người dùng](setting-user-attributes) với dữ liệu đầu vào khi xử lý action. Ví dụ, bạn yêu cầu người dùng nhập tên vào trường văn bản có ID là `name` và muốn đặt giá trị của trường này làm tên của người dùng. Ngoài ra, bạn yêu cầu họ nhập email vào trường `email`. Trong code ứng dụng, nó có thể trông như thế này: ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, onStateUpdatedAction action: AdaptyOnboardingsStateUpdatedAction) { // Store user preferences or responses switch action.params { case .input(let params): // Handle text input let builder = AdaptyProfileParameters.Builder() // Map elementId to appropriate profile field switch action.elementId { case "name": builder.with(firstName: params.value.value) case "email": builder.with(email: params.value.value) default: break } // Delegate methods are synchronous; kick off the async update in a Task. Task { do { try await Adapty.updateProfile(params: builder.build()) } catch { // handle the error } } default: break } } ``` ### Tùy chỉnh paywall dựa trên câu trả lời \{#customize-paywalls-based-on-answers\} Sử dụng quiz trong onboarding, bạn cũng có thể tùy chỉnh các paywall hiển thị cho người dùng sau khi họ hoàn thành onboarding. Ví dụ, bạn có thể hỏi người dùng về kinh nghiệm thể thao của họ và hiển thị các CTA cùng sản phẩm khác nhau cho từng nhóm người dùng. 1. [Thêm quiz](onboarding-quizzes) trong trình tạo onboarding và gán các ID có ý nghĩa cho các tùy chọn. 2. Xử lý các câu trả lời quiz dựa trên ID của chúng và [đặt thuộc tính tùy chỉnh](setting-user-attributes) cho người dùng. ```swift showLineNumbers func onboardingController(_ controller: AdaptyOnboardingController, onStateUpdatedAction action: AdaptyOnboardingsStateUpdatedAction) { // Handle quiz responses and set custom attributes switch action.params { case .select(let params): // Handle quiz selection let builder = AdaptyProfileParameters.Builder() // Map quiz responses to custom attributes switch action.elementId { case "experience": // Set custom attribute 'experience' with the selected value (beginner, amateur, pro) try? builder.with(customAttribute: params.value.value, forKey: "experience") default: break } // Delegate methods are synchronous; kick off the async update in a Task. Task { do { try await Adapty.updateProfile(params: builder.build()) } catch { // handle the error } } default: break } } ``` 3. [Tạo phân khúc](segments) cho từng giá trị thuộc tính tùy chỉnh. 4. Tạo một [placement](placements) và thêm [đối tượng](audience) cho từng phân khúc bạn đã tạo. 5. [Hiển thị paywall](ios-paywalls) cho placement trong code ứng dụng của bạn. Nếu onboarding có nút mở paywall, hãy triển khai code paywall như một [phản hồi cho action của nút đó](ios-handling-onboarding-events#opening-a-paywall). --- # File: ios-sdk-call-order --- --- title: "Thứ tự gọi trong iOS SDK" description: "Tránh mất quyền truy cập premium, thiếu attribution và lỗi #2002 không liên tục bằng cách gọi các phương thức Adapty SDK theo đúng thứ tự." --- `Adapty.activate()` phải hoàn thành trước khi bạn gọi bất kỳ phương thức nào khác của Adapty SDK. Cho đến khi nó hoàn tất, SDK chưa có trạng thái. Bất kỳ lệnh gọi nào được thực hiện trước hoặc song song với `activate()` sẽ thất bại với lỗi [`#2002 notActivated`](ios-sdk-error-handling#network-errors). Nếu ứng dụng của bạn xác thực người dùng và bạn thu thập customer user ID sau khi khởi chạy, hãy gọi `Adapty.identify()` tại thời điểm đó. Đừng gọi các phương thức liên quan đến hành động người dùng cho đến khi `identify` hoàn tất. Các lệnh gọi thực hiện song song với nó sẽ thất bại với lỗi [`#3006 profileWasChanged`](ios-sdk-error-handling#general-errors), hoặc đổ vào hồ sơ người dùng ẩn danh được tạo lúc activation. Khi điều này xảy ra, attribution, MMP ID như `appsflyer_id`, và quyền sở hữu lượt cài đặt không phải lúc nào cũng được chuyển sang hồ sơ người dùng đã xác định. Nếu ứng dụng của bạn không xác thực người dùng, hãy bỏ qua `identify` và tiếp tục làm việc với hồ sơ người dùng ẩn danh. Các SDK MMP và analytics (AppsFlyer, Adjust, Branch, PostHog) tuân theo cùng một quy tắc. Hãy khởi tạo chúng trước và chờ callback UID của chúng trước khi gọi `Adapty.activate`. Nếu không, MMP ID sẽ gắn vào một hồ sơ người dùng ẩn danh tồn tại ngắn và không phải lúc nào cũng được chuyển sang hồ sơ người dùng đã xác định. Về chi tiết cụ thể với AppsFlyer, xem [AppsFlyer](appsflyer). ## Thứ tự đúng \{#the-correct-order\} Lộ trình của bạn phụ thuộc vào hai yếu tố: khi nào bạn biết customer user ID và bạn có sử dụng SDK MMP hoặc analytics hay không. - **Bước 2 và 5**: Bắt buộc cho mọi ứng dụng. Activate SDK, sau đó gọi các phương thức SDK. - **Bước 1 và 3**: Chỉ bắt buộc nếu bạn tích hợp SDK MMP hoặc analytics (AppsFlyer, Adjust, Branch, PostHog). - **Bước 4**: Chỉ bắt buộc nếu ứng dụng của bạn xác thực người dùng và thu thập customer user ID sau khi khởi chạy. Nếu bạn có customer user ID ngay khi ứng dụng khởi chạy, hãy truyền trực tiếp vào `activate()` (bước 2a). Lộ trình này không bao giờ tạo hồ sơ người dùng ẩn danh, do đó bước 4 là không cần thiết. | Bước | Lệnh gọi | Thời điểm | Lưu ý | |------|---------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------| | 1 | Khởi tạo SDK MMP hoặc analytics của bạn (AppsFlyer, Adjust, PostHog, Branch) | Khởi chạy ứng dụng, đầu tiên | Chờ callback UID của MMP, ví dụ `getAppsFlyerUID`. | | 2a | `Adapty.activate(with: config)` với `customerUserId` được đặt trên config | Khởi chạy ứng dụng, sau bước 1, nếu bạn có customer user ID | Được khuyến nghị. Không bao giờ tạo hồ sơ người dùng ẩn danh. | | 2b | `Adapty.activate(with: config)` không có `customerUserId` | Khởi chạy ứng dụng, sau bước 1, nếu bạn không có customer user ID (hoặc không thu thập) | Adapty tạo một hồ sơ người dùng ẩn danh. | | 3 | `Adapty.setIntegrationIdentifier(...)` cho từng MMP | Sau bước 2, trước bất kỳ lệnh gọi hành động người dùng nào | Bắt buộc để MMP ID gắn vào đúng hồ sơ người dùng. | | 4 | `try await Adapty.identify("YOUR_USER_ID")` | Sau bước 3 (hoặc bước 2 nếu không có MMP), trước bước 5 — chỉ trên lộ trình 2b có xác thực | Luôn `await`. Các lệnh gọi đồng thời trong quá trình `identify` sẽ tạo ra lỗi `#3006 profileWasChanged`. | | 5 | `getPaywall`, `getPaywallProducts`, `restorePurchases`, `makePurchase`, `updateAttribution`, `updateProfile` | Sau bước 4 nếu bạn gọi `identify`; nếu không thì sau bước 3 (hoặc bước 2 nếu không có MMP) | Các lệnh gọi này cần một hồ sơ người dùng ổn định. | :::important Bỏ qua các bước này sẽ gây ra mất quyền truy cập premium cho người dùng quay lại, thiếu `appsflyer_id` trên hồ sơ người dùng, và paywall được trả về sai đối tượng. ::: ## Cài đặt web2app và web-funnel \{#web2app-and-web-funnel-installs\} Nếu người dùng mua hàng qua web checkout (Stripe, Paddle, FunnelFox) và sau đó cài đặt ứng dụng native, lần `activate()` đầu tiên trên thiết bị sẽ tạo một hồ sơ người dùng ẩn danh mới. Hồ sơ này không được liên kết với hồ sơ web. Nếu bạn có thể xác định customer user ID trước khi khởi chạy ứng dụng (từ luồng xác thực hoặc install referrer), hãy truyền trực tiếp vào `activate()`. Nếu không, giao dịch mua hàng trên web sẽ không hiển thị trên thiết bị cho đến khi bạn gọi `identify("YOUR_USER_ID")` và sau đó `restorePurchases`. Về metadata cần gửi kèm theo mỗi web checkout, xem: - [Stripe](stripe) - [Paddle](paddle) --- # File: ios-optimize-paywall-fetching --- --- title: "Tối ưu hóa việc tải paywall trong iOS SDK" description: "Tải paywall Adapty đáng tin cậy: thời điểm, bộ nhớ đệm và các mẫu dự phòng cho iOS." --- Một lần tải paywall đáng tin cậy trên iOS cần đảm bảo ba điều: hiển thị nhanh, trả về đúng paywall theo đối tượng, và xử lý dự phòng tốt khi mạng chậm. Các quy tắc dưới đây bao gồm thời điểm, bộ nhớ đệm và các mẫu dự phòng để đạt được điều đó. :::tip Các quy tắc này giả định `Adapty.activate()` và `Adapty.identify()` đã hoàn thành. Xem [Thứ tự gọi trong iOS SDK](ios-sdk-call-order). ::: ## Quy tắc và những lỗi thường gặp \{#rules-and-pitfalls\} | Nên làm | Không nên làm | Lý do | |---|---|---| | Tải placement mà bạn sắp hiển thị. | Prefetch tất cả các placement đồng thời khi khởi chạy. | Bulk prefetch chặn main thread và gây màn hình đen trong thời gian burst. | | Gọi `getPaywall` sau khi attribution có cơ hội hoàn tất — ví dụ, 1–2 giây sau `activate` hoặc sau khi `onProfileUpdate` được kích hoạt. | Gọi `getPaywall` tại `App.init()`. | Attribution chưa được xử lý xong. Paywall sẽ được phân giải theo đối tượng mặc định và bỏ qua các phân khúc cũng như cá nhân hóa ASA mà không có cảnh báo. | | Đặt `loadTimeout` và cấu hình [paywall dự phòng](fallback-paywalls) cho mỗi placement. | Chờ `getPaywall` vô thời hạn. | Nếu không có timeout, người dùng có kết nối kém sẽ thấy màn hình trống cho đến khi mạng phục hồi — hoặc họ sẽ đóng ứng dụng. | Xem [Tải paywalls và sản phẩm](fetch-paywalls-and-products) để tham khảo tham số `fetchPolicy` và `loadTimeout`, và [Placements](placements) để chọn đúng placement. ## Tối ưu cho kết nối kém \{#tune-for-poor-connectivity\} Đối với các thị trường có kết nối kém liên tục (khu vực nông thôn, phương tiện công cộng, các vùng bị ảnh hưởng bởi định tuyến mạng): - Đặt `fetchPolicy: .returnCacheDataElseLoad` cho mọi lần tải ngoại trừ lần đầu tiên. - Cấu hình [paywall dự phòng](fallback-paywalls) cho mỗi placement trong Adapty dashboard. - Đặt `loadTimeout` từ 3–5 giây và chấp nhận phương án dự phòng khi timeout xảy ra. - Không chặn việc hiển thị paywall vào `getProfile()`. Gọi `getPaywall` độc lập để một profile chậm không làm chặn giao diện. --- # File: ios-show-aa-targeted-paywall --- --- title: "Hiển thị paywall được nhắm mục tiêu bởi AA khi khởi chạy lần đầu trong iOS SDK" description: "Chờ attribution từ Apple Ads trước khi yêu cầu paywall trên iOS bằng AdaptyProfile.appliedAttributionSources." --- Attribution từ Apple Ads (AA) được nhận không đồng bộ sau khi gọi `Adapty.activate()`. Nếu bạn gọi `getPaywall` sớm, attribution thường chưa được ghi nhận và Adapty sẽ xử lý placement theo đối tượng mặc định — bỏ qua các paywall được phân khúc theo AA. `AdaptyProfile.appliedAttributionSources` cho phép ứng dụng phát hiện khi nào attribution AA đã được áp dụng vào hồ sơ người dùng, để yêu cầu paywall có thể chờ cho đến khi phân khúc AA được xử lý chính xác. ## Trước khi bắt đầu \{#before-you-start\} Bạn cần: - Adapty iOS SDK **3.17.1** trở lên. - Apple Ads đã được cấu hình cho ứng dụng trong Adapty. Xem [Apple Ads](apple-search-ads). ## Cách hoạt động \{#how-it-works\} Sau khi gọi `Adapty.activate()`, SDK sẽ yêu cầu attribution Apple Ads từ Apple trong nền và chuyển kết quả đến backend của Adapty. Khi AA trở thành nguồn attribution đang hoạt động cho hồ sơ người dùng, SDK sẽ cung cấp một `AdaptyProfile` đã được cập nhật có mảng `appliedAttributionSources` chứa `.appleAds`. Mảng rỗng có thể có nghĩa là: - Attribution Apple Ads chưa được xử lý cho hồ sơ người dùng này. - Chưa có attribution nào đến. Ngay cả với mảng rỗng, `getPaywall` vẫn an toàn để gọi — Adapty sẽ xử lý yêu cầu theo đối tượng phù hợp với trạng thái hồ sơ người dùng hiện tại, thường là đối tượng mặc định. :::important Việc chờ chỉ áp dụng cho **lần khởi chạy đầu tiên**. Sau khi attribution Apple Ads đã được ghi nhận, nó được lưu trữ vĩnh viễn trên hồ sơ người dùng. Ở mọi lần khởi chạy tiếp theo, hồ sơ đã được cache sẵn chứa `.appleAds` trong `appliedAttributionSources`, `didLoadLatestProfile` sẽ kích hoạt với giá trị đó ngay lập tức, và `getPaywall` sẽ trả về paywall được phân khúc theo Apple Ads mà không cần chờ đợi. ::: ## Triển khai \{#implementation\} Ở lần khởi chạy đầu tiên, hãy theo dõi `.appleAds` trong hồ sơ người dùng và áp dụng một timeout cứng — nếu attribution Apple Ads không bao giờ đến, những người dùng đó vẫn cần được xem paywall. 1. **Kích hoạt SDK.** Xem [Cài đặt & cấu hình iOS SDK](sdk-installation-ios). 2. **Đăng ký nhận cập nhật hồ sơ người dùng** bằng cách tuân thủ `AdaptyDelegate` và triển khai `didLoadLatestProfile`. Nếu bạn chưa thiết lập delegate, xem [Lắng nghe cập nhật gói đăng ký](ios-check-subscription-status#listen-to-subscription-updates). 3. **Theo dõi `.appleAds` trong `appliedAttributionSources`.** Khi nó xuất hiện, hãy yêu cầu paywall — Adapty sẽ trả về biến thể được phân khúc theo AA: ```swift extension : AdaptyDelegate { nonisolated func didLoadLatestProfile(_ profile: AdaptyProfile) { if profile.appliedAttributionSources.contains(where: { $0 == .appleAds }) { // load paywall via Adapty.getPaywall(placementId:) } } } ``` 4. **Bắt đầu một bộ đếm thời gian 3–5 giây song song với việc đăng ký.** Nếu bộ đếm thời gian kích hoạt trước khi `.appleAds` xuất hiện, hãy yêu cầu paywall ngay: Bất kỳ đường dẫn nào kích hoạt trước đều nên tải paywall; đường dẫn còn lại nên bị bỏ qua. Sử dụng một cờ trạng thái duy nhất (ví dụ: `hasLoadedPaywall`) để loại trùng lặp để tránh tải paywall hai lần. Cấu hình [paywall dự phòng](fallback-paywalls) cho placement để người dùng không bao giờ bị kẹt nếu yêu cầu mạng thất bại. ## Ví dụ đầy đủ \{#complete-example\} Triển khai bên dưới chạy đua attribution với timeout, tải trước paywall của đối tượng mặc định song song, và trả về paywall phù hợp. Người gọi chỉ cần await một hàm async duy nhất — không cần quản lý delegate hay cờ trạng thái ở phía gọi. `ProfileObserver` là một singleton có thể tái sử dụng, phát các cập nhật hồ sơ người dùng từ `AdaptyDelegate`. `PaywallLoader.getPaywallOrDefault` chạy cuộc đua bằng cách sử dụng `TaskGroup` với structured concurrency: - Nếu attribution đến trong `timeout`, nó trả về paywall được phân khúc qua `getPaywall(placementId:)`. - Nếu `timeout` hết hạn trước, nó trả về paywall đối tượng mặc định đã được tải trước qua `getPaywallForDefaultAudience(placementId:)`. ```swift title="PaywallLoader.swift" /// Demonstrates how to fetch a paywall that depends on attribution being applied, /// falling back to the default-audience paywall if attribution doesn't arrive in time. /// /// Stateless and self-contained: every call kicks off its own default-audience /// prefetch and races it against attribution + segmented fetch. enum PaywallLoader { static func getPaywallOrDefault( placementId: String, timeout: TimeInterval ) async throws -> AdaptyPaywall { struct TimedOut: Error {} // Kick off the default-audience request immediately so it has the full // `timeout` window to load. We'll either cancel it on success or await // its result on timeout — never a duplicate network call. let defaultPaywallTask = Task { try await Adapty.getPaywallForDefaultAudience(placementId: placementId) } do { // Race two child tasks: whichever finishes first wins. let result = try await withThrowingTaskGroup(of: AdaptyPaywall.self) { group in // 1. Wait for attribution, then ask Adapty for the segmented paywall. group.addTask { await waitForAttribution() return try await Adapty.getPaywall(placementId: placementId) } // 2. Time-bomb: throws `TimedOut` after `timeout` seconds. group.addTask { try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000)) throw TimedOut() } guard let value = try await group.next() else { throw CancellationError() } group.cancelAll() // stop the loser (sleeper or the attribution wait). return value } // Segmented paywall won — we no longer need the default-audience prefetch. defaultPaywallTask.cancel() return result } catch is TimedOut { // Attribution didn't apply in time — return the prefetched default // (instant if already done, otherwise we await the in-flight request). return try await defaultPaywallTask.value } } /// Suspends until a profile with the desired attribution source is observed. /// `@Published.values` emits the current profile immediately on subscription, /// so this returns on the first iteration if attribution is already applied. @MainActor private static func waitForAttribution() async { for await profile in ProfileObserver.shared.$profile.values { if profile?.appliedAttributionSources.contains(.appleAds) == true { return } } } } @MainActor final class ProfileObserver: AdaptyDelegate { static let shared = ProfileObserver() @Published private(set) var profile: AdaptyProfile? nonisolated func didLoadLatestProfile(_ profile: AdaptyProfile) { Task { @MainActor [weak self] in self?.profile = profile } } } ``` Kết nối `ProfileObserver` vào `AdaptyDelegate` một lần, sau khi `Adapty.activate()` hoàn tất: ```swift Adapty.delegate = ProfileObserver.shared ``` Gọi từ màn hình splash: ```swift do { let paywall = try await PaywallLoader.getPaywallOrDefault( placementId: "YOUR_PLACEMENT_ID", timeout: 5 ) // present the paywall } catch { // handle the error or show a fallback paywall } ``` Nếu ứng dụng của bạn đã sử dụng `AdaptyDelegate` cho các mục đích khác (ví dụ: [lắng nghe cập nhật gói đăng ký](ios-check-subscription-status#listen-to-subscription-updates)), hãy chuyển tiếp `didLoadLatestProfile` đến `ProfileObserver.shared` từ delegate hiện có của bạn thay vì đặt `Adapty.delegate = ProfileObserver.shared`. --- # File: ios-test --- --- title: "Kiểm tra & phát hành trong iOS SDK" description: "Tìm hiểu cách kiểm tra trạng thái gói đăng ký trong ứng dụng iOS của bạn với Adapty." --- Nếu bạn đã tích hợp Adapty SDK vào ứng dụng iOS của mình, hãy kiểm tra xem mọi thứ đã được thiết lập đúng chưa và các giao dịch mua có hoạt động như mong đợi không. Quá trình này bao gồm kiểm tra cả tích hợp SDK lẫn giao dịch mua thực tế. ## Kiểm tra ứng dụng của bạn \{#test-your-app\} Để kiểm tra toàn diện các in-app purchase, bao gồm kiểm tra trong sandbox và xác thực TestFlight, hãy xem [hướng dẫn kiểm tra](test-purchases-in-sandbox) của chúng tôi. ## Chuẩn bị phát hành \{#prepare-for-release\} Trước khi gửi ứng dụng lên cửa hàng, hãy làm theo [Danh sách kiểm tra phát hành](release-checklist) để xác nhận: - Kết nối cửa hàng và thông báo máy chủ đã được cấu hình - Giao dịch mua hoàn tất và được báo cáo về Adapty - Mức độ truy cập được mở khóa và khôi phục đúng cách - Các yêu cầu về quyền riêng tư và xem xét đã được đáp ứng --- # File: InvalidProductIdentifiers --- --- title: "Cách sửa lỗi Code-1000 noProductIDsFound" description: "Giải quyết lỗi mã định danh sản phẩm không hợp lệ khi quản lý gói đăng ký trong Adapty." --- Lỗi mã 1000, `noProductIDsFound`, có nghĩa là không có sản phẩm nào bạn yêu cầu trên paywall sẵn sàng để mua trong App Store, dù chúng đã được liệt kê ở đó. Đôi khi lỗi này đi kèm với cảnh báo `InvalidProductIdentifiers`. Nếu cảnh báo xuất hiện mà không có lỗi, bạn có thể bỏ qua an toàn. Nếu bạn gặp lỗi `noProductIDsFound`, hãy làm theo các bước sau để khắc phục: ## Bước 1. Kiểm tra bundle ID \{#step-2-check-bundle-id\} --- no_index: true --- 1. Mở [App Store Connect](https://appstoreconnect.apple.com/apps). Chọn ứng dụng của bạn và điều hướng đến phần **General** → **App Information**. 2. Sao chép **Bundle ID** trong mục **General Information**. 3. Mở tab [**App settings** -> **iOS SDK**](https://app.adapty.io/settings/ios-sdk) từ menu trên cùng của Adapty và dán giá trị vừa sao chép vào trường **Bundle ID**. 4. Quay lại trang **App information** trong App Store Connect và sao chép **Apple ID** tại đó. 5. Trên trang [**App settings** -> **iOS SDK**](https://app.adapty.io/settings/ios-sdk) trong Adapty dashboard, dán ID vào trường **Apple app ID**. ## Bước 2. Kiểm tra sản phẩm \{#step-2-check-products\} 1. Vào **App Store Connect** và điều hướng đến [**Monetization** → **Subscriptions**](https://appstoreconnect.apple.com/apps/6477523342/distribution/subscriptions) trong menu bên trái. 2. Nhấp vào tên nhóm gói đăng ký. Bạn sẽ thấy các sản phẩm được liệt kê trong mục **Subscriptions**. 3. Đảm bảo sản phẩm bạn đang kiểm tra được đánh dấu là **Ready to Submit**. Nếu chưa, hãy làm theo hướng dẫn trên trang [Product in App Store](app-store-products). 4. So sánh ID sản phẩm trong bảng với ID trong tab [**Products**](https://app.adapty.io/products) trên Adapty Dashboard. Nếu các ID không khớp, hãy sao chép ID sản phẩm từ bảng và [tạo sản phẩm](create-product) với ID đó trong Adapty Dashboard. ## Bước 3. Kiểm tra tính khả dụng của sản phẩm \{#step-4-check-product-availability\} 1. Quay lại **App Store Connect** và mở lại mục **Subscriptions**. 2. Nhấp vào tên nhóm gói đăng ký để xem các sản phẩm. 3. Chọn sản phẩm bạn đang kiểm tra. 4. Cuộn xuống mục **Availability** và kiểm tra rằng tất cả các quốc gia và khu vực cần thiết đều được liệt kê. ## Bước 4. Kiểm tra giá sản phẩm \{#step-5-check-product-prices\} 1. Quay lại mục **Monetization** → **Subscriptions** trong **App Store Connect**. 2. Nhấp vào tên nhóm gói đăng ký. 3. Chọn sản phẩm bạn đang kiểm tra. 4. Cuộn xuống **Subscription Pricing** và mở rộng mục **Current Pricing for New Subscribers**. 5. Đảm bảo tất cả các mức giá cần thiết đều được liệt kê. ## Bước 5. Kiểm tra trạng thái thanh toán, tài khoản ngân hàng và biểu mẫu thuế còn hiệu lực \{#step-5-check-app-paid-status-bank-account-and-tax-forms-are-active\} 1. Trên trang chủ [**App Store Connect**](https://appstoreconnect.apple.com/), nhấp vào **Business**. 2. Chọn tên công ty của bạn. 3. Cuộn xuống và kiểm tra rằng **Paid Apps Agreement**, **Bank Account** và **Tax forms** đều hiển thị trạng thái **Active**. Bằng cách làm theo các bước trên, bạn sẽ có thể khắc phục cảnh báo `InvalidProductIdentifiers` và đưa sản phẩm lên cửa hàng. ## Bước 6. Tạo lại sản phẩm nếu bị kẹt \{#step-6-recreate-the-product-if-its-stuck\} Các bước 1–5 có thể đều vượt qua — trạng thái `Approved`, Bundle ID khớp, API key hợp lệ — nhưng SDK vẫn trả về lỗi `1000 noProductIDsFound`. Trong trường hợp đó, sản phẩm có thể đang bị kẹt trong registry của Apple. Registry sản phẩm của Apple đôi khi rơi vào trạng thái mà sản phẩm tồn tại trong giao diện App Store Connect nhưng không hiển thị qua đường dẫn tra cứu StoreKit. Hãy xóa sản phẩm trong App Store Connect và tạo lại với cùng ID sản phẩm đó. Sau khi tạo lại, hãy chờ tối đa 24 giờ để thay đổi được áp dụng. --- # File: cantMakePayments --- --- title: "Khắc phục lỗi Code-1003 cantMakePayment" description: "Giải quyết lỗi thanh toán khi quản lý gói đăng ký trong Adapty." --- Lỗi 1003, `cantMakePayments`, cho biết thiết bị không thể thực hiện in-app purchase. Nếu bạn gặp lỗi `cantMakePayments`, thường là do một trong các nguyên nhân sau: - Giới hạn thiết bị: Lỗi này không liên quan đến Adapty. Xem cách khắc phục bên dưới. - Cấu hình Observer mode: Không thể dùng đồng thời phương thức `makePurchase` và Observer mode. Xem phần bên dưới. ## Sự cố: Giới hạn thiết bị \{#issue-device-restrictions\} | Sự cố | Giải pháp | |-------------------------------|-----------------------------------------------------------------------------------------------------------------------------| | Giới hạn Screen Time | Tắt giới hạn In-App Purchase trong [Screen Time](https://support.apple.com/en-us/102470) | | Tài khoản bị tạm khóa | Liên hệ Apple Support để giải quyết vấn đề tài khoản | | Giới hạn khu vực | Sử dụng tài khoản App Store từ vùng được hỗ trợ | ## Sự cố: Dùng đồng thời Observer mode và makePurchase \{#issue-using-both-observer-mode-and-makepurchase\} Nếu bạn đang dùng `makePurchases` để xử lý giao dịch mua, bạn không cần dùng Observer mode. [Observer mode](observer-vs-full-mode) chỉ cần thiết khi bạn tự triển khai logic mua hàng. Vì vậy, nếu bạn đang dùng `makePurchase`, bạn có thể xóa phần kích hoạt Observer mode khỏi code khởi tạo SDK một cách an toàn. --- # File: migration-to-ios-sdk-v4 --- --- 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 paywall API bằng flow API, tương thích với cả Flow Builder và Paywall Builder." --- Adapty iOS SDK 4.0 (beta) giới thiệu flow và đổi tên các paywall API cho phù hợp. 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 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:` | | `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 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 bỏ 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 sạch, rồi thêm package trong Xcode qua **File → Add Package Dependency** với URL `https://github.com/adaptyteam/AdaptySDK-iOS.git`. ## Các API đã bị xóa \{#removed-apis\} - **`Adapty.getPaywallProductsWithoutDeterminingOffer(paywall:)`** — đã xóa. Tất cả sản phẩm giờ đây đã bao gồm thông tin ưu đãi, nên bước kiểm tra tính đủ điều kiện riêng biệt không còn cần thiết nữa. - **`AdaptyPaywallProductWithoutDeterminingOffer`** — đã xóa. Các callback trước đây nhận kiểu này (chẳng hạn `didSelectProduct`) giờ nhận `AdaptyPaywallProduct`. ## Tạm thời xóa tính năng mua in-app purchase được quảng bá trên App Store \{#app-store-promoted-in-app-purchases-temporarily-removed\} Trong quá trình migrate sang StoreKit 2, Adapty iOS SDK 4.0 xóa 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 xóa này chỉ là tạm thời — hỗ trợ in-app purchase được quảng bá sẽ quay trở lại trong một bản phát hành 4.x tiếp theo. Nếu ứng dụng của bạn phụ thuộc vào in-app purchase được quảng bá, hãy tiếp tục dùng iOS SDK 3.x cho đến khi tính năng này được khôi phục. ::: ## Lấy paywall \{#fetching-paywalls\} ### getPaywall + getPaywallConfiguration → getFlow + getFlowConfiguration \{#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à sang `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:) \{#getpaywallproductspaywall--getpaywallproductsflow\} `getPaywallProducts` giờ nhận `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_--logshowflow_\} `logShowPaywall` được đổi tên thành `logShowFlow` và giờ nhận `AdaptyFlow` thay vì `AdaptyPaywall`. Sự kiện vẫn được ghi lại theo cùng một biến thể, vì vậy 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 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ị 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 đó. ## UIKit \{#uikit\} ### AdaptyPaywallController → AdaptyFlowController \{#adaptypaywall controller--adaptyflo wcontroller\} Đổ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 \{#adaptypaywall controllerdelegate--adaptyflo wcontrollerdelegate\} Đổi tên protocol và cập nhật mọi chữ ký phương thức. Lưu ý rằng `didSelectProduct` giờ nhận `AdaptyPaywallProduct` thay vì `AdaptyPaywallProductWithoutDeterminingOffer` đã bị xó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 và cập nhật tên tham số cấu hình: ```diff showLineNumbers @State var flowPresented = false // đổi tên tùy ý — tên biến là do bạn chọn var body: some View { Text("Hello, AdaptyUI!") - .paywall( + .flow( isPresented: $flowPresented, - paywallConfiguration: paywallConfiguration, + flowConfiguration: flowConfiguration, 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 các 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 trên `AdaptyUIError` code `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 \{#adaptypaywall view--adaptyflo wview\} Đổi tên view, cập nhật tham số cấu hình, và cập nhật bất kỳ closure `didSelectProduct` nào — giờ nó nhận `AdaptyPaywallProduct` thay vì `AdaptyPaywallProductWithoutDeterminingOffer` đã bị xóa: ```diff showLineNumbers - AdaptyPaywallView( - paywallConfiguration: paywallConfiguration, - didSelectProduct: { product: AdaptyPaywallProductWithoutDeterminingOffer in /* handle */ }, + AdaptyFlowView( + flowConfiguration: flowConfiguration, + didSelectProduct: { product: AdaptyPaywallProduct in /* handle */ }, 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 */ } ) ``` ## Asset tùy chỉnh của AdaptyUI \{#adaptyui-custom-assets\} ### AdaptyUICustomVideoAsset \{#adaptyuicustomvideoasset\} Có hai thay đổi ảnh hưởng đến mọi call site hiện có: - `.player` giờ nhận `AVPlayer` thay vì `AVQueuePlayer`. - Mỗi case được thêm tham số cuối `resolution: CGSize?`. 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 trước không gian layout (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?) ``` ## Attribution và integration identifier \{#attribution-and-integration-identifiers\} ### updateAttribution(_:source:) \{#updateattribution_source\} Tham số `source` thay đổi từ `String` sang kiểu `AdaptyAttributionSource` mới, và `AdaptyProfile.AttributionSource` trước đây được lồng bên trong giờ đượ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 string literal cho bất kỳ source nào khác — `AdaptyAttributionSource` tuân thủ `ExpressibleByStringLiteral`, nên các lệnh gọi dùng string literal hiện có vẫn biên dịch được. ```diff showLineNumbers - try await Adapty.updateAttribution(attribution, source: "adjust") + try await Adapty.updateAttribution(attribution, source: .adjust) ``` Các source được định nghĩa sẵn: `.appleAds`, `.adjust`, `.appsflyer`, `.branch`, `.tenjin`. Nếu bạn lưu source trong biến `String`, hãy wrap nó lại: `AdaptyAttributionSource(rawValue: yourSource)`. ### setIntegrationIdentifier(_:) \{#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`. Sử dụng các factory method được định nghĩa sẵn thay vì raw string key: ```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ế mỗi chuỗi key cũ bằng factory method tương ứng: | Key v3 | Factory v4 | |---|---| | `"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(_:)` | --- # File: migration-to-ios-315 --- --- title: "Migrate Adapty iOS SDK to v3.15" description: "Migrate to Adapty iOS SDK v3.15 for better performance and new monetization features." --- Nếu bạn đang dùng [Paywall Builder](adapty-paywall-builder) trong [Observer mode](observer-vs-full-mode), bắt đầu từ iOS SDK 3.15, bạn cần triển khai một phương thức mới là `observerModeDidInitiateRestorePurchases(onStartRestore:onFinishRestore:)`. Phương thức này cung cấp khả năng kiểm soát chi tiết hơn đối với logic khôi phục, cho phép bạn xử lý việc khôi phục giao dịch mua trong flow tùy chỉnh của mình. Để biết chi tiết triển khai đầy đủ, tham khảo [Hiển thị paywall Paywall Builder trong Observer mode](ios-present-paywall-builder-paywalls-in-observer-mode). ```diff showLineNumbers func observerMode(didInitiatePurchase product: AdaptyPaywallProduct, onStartPurchase: @escaping () -> Void, onFinishPurchase: @escaping () -> Void) { // use the product object to handle the purchase // use the onStartPurchase and onFinishPurchase callbacks to notify AdaptyUI about the process of the purchase } + func observerModeDidInitiateRestorePurchases(onStartRestore: @escaping () -> Void, + onFinishRestore: @escaping () -> Void) { + // use the onStartRestore and onFinishRestore callbacks to notify AdaptyUI about the process of the restore + } ``` --- # File: migration-to-ios-sdk-34 --- --- title: "Migrate Adapty iOS SDK to v3.4" description: "Migrate to Adapty iOS SDK v3.4 for better performance and new monetization features." --- Adapty SDK 3.4.0 là một bản phát hành lớn, giới thiệu các cải tiến yêu cầu bạn thực hiện các bước migration. ## Cập nhật khởi tạo Adapty SDK \{#update-adapty-sdk-activation\} ```diff showLineNumbers // In your AppDelegate class: let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") - Adapty.activate(with: configurationBuilder) { error in + Adapty.activate(with: configurationBuilder.build()) { error in // handle the error } ``` **Cập nhật file paywall dự phòng** Cập nhật các file paywall dự phòng để đảm bảo tương thích với phiên bản SDK mới: 1. [Tải xuống các file paywall dự phòng đã được cập nhật](fallback-paywalls) từ Adapty Dashboard. 2. [Thay thế các paywall dự phòng hiện có trong ứng dụng mobile của bạn](ios-use-fallback-paywalls) bằng các file mới. ```diff showLineNumbers @main struct SampleApp: App { init() { let configurationBuilder = AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") Task { - try await Adapty.activate(with: configurationBuilder) + try await Adapty.activate(with: configurationBuilder.build()) } } var body: some Scene { WindowGroup { ContentView() } } } ``` **Cập nhật file paywall dự phòng** Cập nhật các file paywall dự phòng để đảm bảo tương thích với phiên bản SDK mới: 1. [Tải xuống các file paywall dự phòng đã được cập nhật](fallback-paywalls) từ Adapty Dashboard. 2. [Thay thế các paywall dự phòng hiện có trong ứng dụng mobile của bạn](ios-use-fallback-paywalls) bằng các file mới. --- # File: migration-to-ios330 --- --- title: "Migrate Adapty iOS SDK sang v3.3" description: "Migrate lên Adapty iOS SDK v3.3 để cải thiện hiệu năng và sử dụng các tính năng kiếm tiền mới." --- Adapty SDK 3.3.0 là một bản phát hành lớn mang lại nhiều cải tiến, tuy nhiên bạn có thể cần thực hiện một số bước migration. 1. Đổi tên `Adapty.Configuration` thành `AdaptyConfiguration`. 2. Đổi tên phương thức `getViewConfiguration` thành `getPaywallConfiguration`. 3. Xóa các tham số `didCancelPurchase` và `paywall` khỏi SwiftUI, đồng thời đổi tên tham số `viewConfiguration` thành `paywallConfiguration`. 4. Cập nhật cách xử lý promotional in-app purchase từ App Store bằng cách xóa tham số `defermentCompletion` khỏi phương thức `AdaptyDelegate`. 5. Xóa phương thức `getProductsIntroductoryOfferEligibility`. 6. Cập nhật cấu hình tích hợp cho Adjust, AirBridge, Amplitude, AppMetrica, Appsflyer, Branch, Facebook Ads, Firebase và Google Analytics, Mixpanel, OneSignal, Pushwoosh. 7. Cập nhật cách triển khai Observer mode.
## Đổi tên Adapty.Configuration thành AdaptyConfiguration \{#rename-adaptyconfiguration-to-adaptyconfiguration\} Cập nhật code khởi động Adapty iOS SDK như sau: ```diff showLineNumbers // In your AppDelegate class: let configurationBuilder = - Adapty.Configuration + AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(observerMode: false) .with(customerUserId: "YOUR_USER_ID") .with(idfaCollectionDisabled: false) .with(ipAddressCollectionDisabled: false) Adapty.activate(with: configurationBuilder) { error in // handle the error } ``` ```diff showLineNumbers @main struct SampleApp: App { init() let configurationBuilder = - Adapty.Configuration + AdaptyConfiguration .builder(withAPIKey: "PUBLIC_SDK_KEY") .with(observerMode: false) // optional .with(customerUserId: "YOUR_USER_ID") // optional .with(idfaCollectionDisabled: false) // optional .with(ipAddressCollectionDisabled: false) // optional Task { try await Adapty.activate(with: configurationBuilder) } } var body: some Scene { WindowGroup { ContentView() } } } ``` ## Đổi tên phương thức getViewConfiguration thành getPaywallConfiguration \{#rename-getviewconfiguration-method-to-getpaywallconfiguration\} Cập nhật tên phương thức để lấy `viewConfiguration` của paywall: ```diff showLineNumbers guard paywall.hasViewConfiguration else { // use your custom logic return } do { - let paywallConfiguration = try await AdaptyUI.getViewConfiguration( + let paywallConfiguration = try await AdaptyUI.getPaywallConfiguration( forPaywall: paywall ) // use loaded configuration } catch { // handle the error } ``` Để biết thêm chi tiết về phương thức này, hãy xem [Lấy view configuration của paywall được thiết kế bằng Paywall Builder](get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder). ## Thay đổi tham số trong SwiftUI \{#change-parameters-in-swiftui\} Các cập nhật sau đã được thực hiện với SwiftUI: 1. Tham số `didCancelPurchase` đã bị xóa. Hãy dùng `didFinishPurchase` thay thế. 2. Phương thức `.paywall()` không còn nhận đối tượng paywall nữa. 3. Tham số `paywallConfiguration` đã thay thế tham số `viewConfiguration`. Cập nhật code của bạn như sau: ```diff showLineNumbers @State var paywallPresented = false var body: some View { Text("Hello, AdaptyUI!") .paywall( isPresented: $paywallPresented, - paywall: , - viewConfiguration: , + paywallConfiguration: , didPerformAction: { action in switch action { case .close: paywallPresented = false default: // Handle other actions break } }, - didFinishPurchase: { product, profile in paywallPresented = false }, + didFinishPurchase: { product, purchaseResult in /* handle the result*/ }, didFailPurchase: { product, error in /* handle the error */ }, didFinishRestore: { profile in /* check access level and dismiss */ }, didFailRestore: { error in /* handle the error */ }, didFailRendering: { error in paywallPresented = false } - didCancelPurchase: { product in /* handle the result*/} ) } ``` ## Cập nhật cách xử lý promotional in-app purchase từ App Store \{#update-handling-of-promotional-in-app-purchases-from-app-store\} Cập nhật cách xử lý promotional in-app purchase từ App Store bằng cách xóa tham số `defermentCompletion` khỏi phương thức `AdaptyDelegate`, như ví dụ dưới đây: ```swift showLineNumbers title="Swift" final class YourAdaptyDelegateImplementation: AdaptyDelegate { nonisolated func shouldAddStorePayment(for product: AdaptyDeferredProduct) -> Bool { // 1a. // Return `true` to continue the transaction in your app. // 1b. // Store the product object and return `false` to defer or cancel the transaction. false } // 2. Continue the deferred purchase later on by passing the product to `makePurchase` func continueDeferredPurchase() async { let storedProduct: AdaptyDeferredProduct = // get the product object from the 1b. do { try await Adapty.makePurchase(product: storedProduct) } catch { // handle the error } } } ``` ## Xóa phương thức getProductsIntroductoryOfferEligibility \{#remove-getproductsintroductoryoffereligibility-method\} Trước Adapty iOS SDK 3.3.0, đối tượng sản phẩm luôn bao gồm các ưu đãi, bất kể người dùng có đủ điều kiện hay không. Bạn phải tự kiểm tra điều kiện hợp lệ trước khi sử dụng ưu đãi. Hiện tại, đối tượng sản phẩm chỉ bao gồm ưu đãi khi người dùng đủ điều kiện. Điều này có nghĩa là bạn không cần kiểm tra điều kiện hợp lệ nữa — nếu có ưu đãi, người dùng đã đủ điều kiện. Nếu bạn vẫn muốn xem ưu đãi cho những người dùng không đủ điều kiện, hãy tham khảo `sk1Product` và `sk2Product`. ## Cập nhật cấu hình SDK tích hợp bên thứ ba \{#update-third-party-integration-sdk-configuration\} Bắt đầu từ Adapty iOS SDK 3.3.0, chúng tôi đã cập nhật API công khai cho phương thức `updateAttribution`. Trước đây, nó nhận dictionary `[AnyHashable: Any]`, cho phép bạn truyền trực tiếp các đối tượng attribution từ nhiều dịch vụ khác nhau. Hiện tại, nó yêu cầu `[String: any Sendable]`, vì vậy bạn cần chuyển đổi các đối tượng attribution trước khi truyền vào. Để đảm bảo các tích hợp hoạt động đúng với Adapty iOS SDK 3.3.0 trở lên, hãy cập nhật cấu hình SDK cho các tích hợp sau theo hướng dẫn trong các phần bên dưới. ### Adjust Cập nhật code ứng dụng của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem [Cấu hình SDK cho tích hợp Adjust](adjust#connect-your-app-to-adjust). ```diff showLineNumbers class AdjustModuleImplementation { - func updateAdjustAttribution() { - Adjust.attribution { attribution in - guard let attributionDictionary = attribution?.dictionary()?.toSendableDict() else { return } - - Adjust.adid { adid in - guard let adid else { return } - - Adapty.updateAttribution(attributionDictionary, source: .adjust, networkUserId: adid) { error in - // handle the error - } - } - } - } + func updateAdjustAdid() { + Adjust.adid { adid in + guard let adid else { return } + + Adapty.setIntegrationIdentifier(key: "adjust_device_id", value: adid) + } + } + + func updateAdjustAttribution() { + Adjust.attribution { attribution in + guard let attribution = attribution?.dictionary() else { + return + } + + Adapty.updateAttribution(attribution, source: "adjust") + } + } } ``` ```diff showLineNumbers class YourAdjustDelegateImplementation { // Find your implementation of AdjustDelegate // and update adjustAttributionChanged method: func adjustAttributionChanged(_ attribution: ADJAttribution?) { - if let attribution = attribution?.dictionary()?.toSendableDict() { - Adapty.updateAttribution(attribution, source: .adjust) + if let attribution = attribution?.dictionary() { + Adapty.updateAttribution(attribution, source: "adjust") } } } ``` ### AirBridge Cập nhật code ứng dụng của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem [Cấu hình SDK cho tích hợp AirBridge](airbridge#connect-your-app-to-airbridge). ```diff showLineNumbers import AirBridge - let builder = AdaptyProfileParameters.Builder() - .with(airbridgeDeviceId: AirBridge.deviceUUID()) - - Adapty.updateProfile(params: builder.build()) + do { + try await Adapty.setIntegrationIdentifier( + key: "airbridge_device_id", + value: AirBridge.deviceUUID() + ) + } catch { + // handle the error + } ``` ### Amplitude Cập nhật code ứng dụng của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem [Cấu hình SDK cho tích hợp Amplitude](amplitude#sdk-configuration). ```diff showLineNumbers import Amplitude - let builder = AdaptyProfileParameters.Builder() - .with(amplitudeUserId: Amplitude.instance().userId) - .with(amplitudeDeviceId: Amplitude.instance().deviceId) - - Adapty.updateProfile(params: builder.build()) + do { + try await Adapty.setIntegrationIdentifier( + key: "amplitude_user_id", + value: Amplitude.instance().userId + ) + try await Adapty.setIntegrationIdentifier( + key: "amplitude_device_id", + value: Amplitude.instance().deviceId + ) + } catch { + // handle the error + } ``` ### AppMetrica Cập nhật code ứng dụng của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem [Cấu hình SDK cho tích hợp AppMetrica](appmetrica#sdk-configuration). ```diff showLineNumbers import AppMetricaCore - if let deviceID = AppMetrica.deviceID { - let builder = AdaptyProfileParameters.Builder() - .with(appmetricaDeviceId: deviceID) - .with(appmetricaProfileId: "YOUR_ADAPTY_CUSTOMER_USER_ID") - - Adapty.updateProfile(params: builder.build()) - } + if let deviceID = AppMetrica.deviceID { + do { + try await Adapty.setIntegrationIdentifier( + key: "appmetrica_device_id", + value: deviceID + ) + try await Adapty.setIntegrationIdentifier( + key: "appmetrica_profile_id", + value: "YOUR_ADAPTY_CUSTOMER_USER_ID" + ) + } catch { + // handle the error + } + } ``` ### AppsFlyer Cập nhật code ứng dụng của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem [Cấu hình SDK cho tích hợp AppsFlyer](appsflyer#connect-your-app-to-appsflyer). ```diff showLineNumbers class YourAppsFlyerLibDelegateImplementation { // Find your implementation of AppsFlyerLibDelegate // and update onConversionDataSuccess method: func onConversionDataSuccess(_ conversionInfo: [AnyHashable : Any]) { let uid = AppsFlyerLib.shared().getAppsFlyerUID() - Adapty.updateAttribution( - conversionInfo.toSendableDict(), - source: .appsflyer, - networkUserId: uid - ) + Adapty.setIntegrationIdentifier(key: "appsflyer_id", value: uid) + Adapty.updateAttribution(conversionInfo, source: "appsflyer") } } ``` ### Branch Cập nhật code ứng dụng của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem [Cấu hình SDK cho tích hợp Branch](branch#connect-your-app-to-branch). ```diff showLineNumbers class YourBranchImplementation { func initializeBranch() { // Pass the attribution you receive from the initializing method of Branch iOS SDK to Adapty. Branch.getInstance().initSession(launchOptions: launchOptions) { (data, error) in - if let data = data?.toSendableDict() { - Adapty.updateAttribution(data, source: .branch) - } + if let data { + Adapty.updateAttribution(data, source: "branch") + } } } } ``` ### Facebook Ads Cập nhật code ứng dụng của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem [Cấu hình SDK cho tích hợp Facebook Ads](facebook-ads#connect-your-app-to-facebook-ads). ```diff showLineNumbers import FacebookCore - let builder = AdaptyProfileParameters.Builder() - .with(facebookAnonymousId: AppEvents.shared.anonymousID) - - do { - try Adapty.updateProfile(params: builder.build()) - } catch { - // handle the error - } + do { + try await Adapty.setIntegrationIdentifier( + key: "facebook_anonymous_id", + value: AppEvents.shared.anonymousID + ) + } catch { + // handle the error + } ``` ### Firebase và Google Analytics Cập nhật code ứng dụng của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem [Cấu hình SDK cho tích hợp Firebase và Google Analytics](firebase-and-google-analytics). ```diff showLineNumbers import FirebaseCore import FirebaseAnalytics FirebaseApp.configure() - if let appInstanceId = Analytics.appInstanceID() { - let builder = AdaptyProfileParameters.Builder() - .with(firebaseAppInstanceId: appInstanceId) - Adapty.updateProfile(params: builder.build()) { error in - // handle error - } - } + if let appInstanceId = Analytics.appInstanceID() { + do { + try await Adapty.setIntegrationIdentifier( + key: "firebase_app_instance_id", + value: appInstanceId + ) + } catch { + // handle the error + } + } ``` ### Mixpanel Cập nhật code ứng dụng của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem [Cấu hình SDK cho tích hợp Mixpanel](mixpanel#sdk-configuration). ```diff showLineNumbers import Mixpanel - let builder = AdaptyProfileParameters.Builder() - .with(mixpanelUserId: Mixpanel.mainInstance().distinctId) - - do { - try await Adapty.updateProfile(params: builder.build()) - } catch { - // handle the error - } + do { + try await Adapty.setIntegrationIdentifier( + key: "mixpanel_user_id", + value: Mixpanel.mainInstance().distinctId + ) + } catch { + // handle the error + } ``` ### OneSignal Cập nhật code ứng dụng của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem [Cấu hình SDK cho tích hợp OneSignal](onesignal#sdk-configuration). ```diff showLineNumbers // PlayerID (pre-v5 OneSignal SDK) // in your OSSubscriptionObserver implementation func onOSSubscriptionChanged(_ stateChanges: OSSubscriptionStateChanges) { if let playerId = stateChanges.to.userId { - let params = AdaptyProfileParameters.Builder() - .with(oneSignalPlayerId: playerId) - .build() - - Adapty.updateProfile(params:params) { error in - // check error - } + Task { + try await Adapty.setIntegrationIdentifier( + key: "one_signal_player_id", + value: playerId + ) + } } } // SubscriptionID (v5+ OneSignal SDK) OneSignal.Notifications.requestPermission({ accepted in - let id = OneSignal.User.pushSubscription.id - - let builder = AdaptyProfileParameters.Builder() - .with(oneSignalSubscriptionId: id) - - Adapty.updateProfile(params: builder.build()) + Task { + try await Adapty.setIntegrationIdentifier( + key: "one_signal_subscription_id", + value: OneSignal.User.pushSubscription.id + ) + } }, fallbackToSettings: true) ``` ### Pushwoosh Cập nhật code ứng dụng của bạn như bên dưới. Để xem ví dụ code đầy đủ, hãy xem [Cấu hình SDK cho tích hợp Pushwoosh](pushwoosh#sdk-configuration). ```diff showLineNumbers - let params = AdaptyProfileParameters.Builder() - .with(pushwooshHWID: Pushwoosh.sharedInstance().getHWID()) - .build() - - Adapty.updateProfile(params: params) { error in - // handle the error - } + do { + try await Adapty.setIntegrationIdentifier( + key: "pushwoosh_hwid", + value: Pushwoosh.sharedInstance().getHWID() + ) + } catch { + // handle the error + } ``` ## Cập nhật cách triển khai Observer mode \{#update-observer-mode-implementation\} Cập nhật cách bạn liên kết paywall với giao dịch. Trước đây, bạn dùng phương thức `setVariationId` để gán `variationId`. Hiện tại, bạn có thể đưa `variationId` trực tiếp khi ghi lại giao dịch bằng phương thức `reportTransaction` mới. Xem ví dụ code hoàn chỉnh tại [Liên kết paywall với giao dịch mua trong Observer mode](report-transactions-observer-mode). :::warning Nhớ ghi lại giao dịch bằng phương thức `reportTransaction`. Nếu bỏ qua bước này, Adapty sẽ không nhận ra giao dịch, không cấp mức độ truy cập, không đưa vào analytics, cũng không gửi đến các tích hợp. Đây là bước bắt buộc! ::: ```diff showLineNumbers - let variationId = paywall.variationId - - // There are two overloads: for StoreKit 1 and StoreKit 2 - Adapty.setVariationId(variationId, forPurchasedTransaction: transaction) { error in - if error == nil { - // successful binding - } - } + do { + // every time when calling transaction.finish() + try await Adapty.reportTransaction(transaction, withVariationId: ) + } catch { + // handle the error + } ``` --- # File: migration-to-ios-sdk-v3 --- --- title: "Migrate Adapty iOS SDK to v3.0" description: "Migrate sang Adapty iOS SDK v3.0 để có hiệu suất tốt hơn và các tính năng kiếm tiền mới." --- Adapty SDK v3.0 hỗ trợ [Adapty Paywall Builder](adapty-paywall-builder) mới — công cụ no-code thân thiện với người dùng để tạo paywall. Với tính linh hoạt tối đa và khả năng thiết kế phong phú, các paywall của bạn sẽ trở nên hiệu quả và sinh lời hơn. :::info Lưu ý rằng thư viện AdaptyUI đã bị deprecated và hiện được tích hợp trực tiếp vào AdaptySDK. ::: ## Cài đặt lại Adapty SDK v3.x qua Swift Package Manager \{#reinstall-adapty-sdk-v3x-via-swift-package-manager\} 1. Xóa package dependency AdaptyUI SDK khỏi project của bạn, bạn sẽ không cần nó nữa. 2. Dù bạn đã có sẵn, bạn vẫn cần thêm lại dependency Adapty SDK. Để làm điều này, trong Xcode, mở **File** -> **Add Package Dependency...**. Lưu ý rằng cách thêm package dependency có thể khác nhau tùy phiên bản Xcode. Tham khảo tài liệu Xcode nếu cần. 3. Nhập URL repository `https://github.com/adaptyteam/AdaptySDK-iOS.git` 4. Chọn phiên bản và nhấn nút **Add package**. 5. Chọn các module bạn cần: 1. **Adapty** là module bắt buộc 2. **AdaptyUI** là module tùy chọn, cần thiết nếu bạn dự định sử dụng [Adapty Paywall Builder](adapty-paywall-builder). 6. Xcode sẽ thêm package dependency vào project của bạn và bạn có thể import nó. Trong cửa sổ **Choose Package Products**, nhấn nút **Add package** một lần nữa. Package sẽ xuất hiện trong danh sách **Packages**. ## Cài đặt lại Adapty SDK v3.x qua CocoaPods \{#reinstall-adapty-sdk-v3x-via-cocoapods\} 1. Thêm Adapty vào `Podfile` của bạn. Chọn các module bạn cần: 1. **Adapty** là module bắt buộc. 2. **AdaptyUI** là module tùy chọn, cần thiết nếu bạn dự định sử dụng [Adapty Paywall Builder](adapty-paywall-builder). 2. ```shell showLineNumbers title="Podfile" pod 'Adapty', '~> 3.2.0' pod 'AdaptyUI', '~> 3.2.0' # optional module needed only for Paywall Builder ``` 3. Chạy lệnh: ```sh showLineNumbers title="Shell" pod install ``` Lệnh này tạo ra file `.xcworkspace` cho app của bạn. Sử dụng file này cho toàn bộ quá trình phát triển ứng dụng sau này. Kích hoạt các module Adapty và AdaptyUI SDK. Trước v3.0, bạn không cần kích hoạt AdaptyUI, vì vậy hãy nhớ **thêm phần kích hoạt AdaptyUI**. Các tham số không thay đổi, hãy giữ nguyên chúng. ```swift showLineNumbers // In your AppDelegate class: let configurationBuilder = AdaptyConfiguration .Builder(withAPIKey: "PUBLIC_SDK_KEY") .with(observerMode: false) .with(customerUserId: "YOUR_USER_ID") .with(idfaCollectionDisabled: false) .with(ipAddressCollectionDisabled: false) Adapty.activate(with: configurationBuilder) { error in // handle the error } // Only if you are going to use AdaptyUI AdaptyUI.activate() ``` ```swift title="" showLineNumbers @main struct SampleApp: App { init() let configurationBuilder = AdaptyConfiguration .Builder(withAPIKey: "PUBLIC_SDK_KEY") .with(observerMode: false) // optional .with(customerUserId: "YOUR_USER_ID") // optional .with(idfaCollectionDisabled: false) // optional .with(ipAddressCollectionDisabled: false) // optional Adapty.activate(with: configurationBuilder) { error in // handle the error } // Only if you are going to use AdaptyUI AdaptyUI.activate() } var body: some Scene { WindowGroup { ContentView() } } } ``` --- # End of Documentation _Generated on: 2026-07-01T16:30:13.682Z_ _Successfully processed: 44/44 files_