# FLUTTER - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Locale: vi Generated on: 2026-07-24T13:01:43.412Z Total files: 44 --- # File: sdk-installation-flutter --- --- title: "Cài đặt & cấu hình Flutter SDK" description: "Hướng dẫn từng bước cài đặt Adapty SDK trên Flutter 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 liền mạch vào ứng dụng Flutter của bạn: - **Core Adapty**: Đây là SDK cốt lõi bắt buộc để Adapty hoạt động đúng trong ứng dụng của bạn. - **AdaptyUI**: Module này 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 với người dùng giúp tạo paywall đa nền tảng một cách 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 di động? Hãy xem [ứng dụng mẫu](https://github.com/adaptyteam/AdaptySDK-Flutter/tree/master/example) 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. ::: ## Yêu cầu \{#requirements\} Adapty SDK hỗ trợ iOS 13.0+, nhưng yêu cầu iOS 15.0+ để hoạt động đúng với các paywall được tạo trong Paywall Builder. Adapty Flutter SDK 4.0 — bổ sung hỗ trợ [Flow Builder](adapty-flow-builder) — nâng yêu cầu tối thiểu lên **iOS 15.0+**, **Xcode 26+** và **Flutter 3.32.0+** (Dart 3.8.0+). Xem [Adapty SDK 4.0](#adapty-sdk-40-swift-package-manager) bên dưới để biết chi tiết cài đặt. :::info Adapty tương thích với Google Play Billing Library lên đến phiên bản 8.x. Theo mặc định, Adapty hoạt động với Google Play Billing Library v7.0.0, nhưng nếu bạn muốn sử dụng phiên bản mới hơn, bạn có thể tự [thêm dependency](https://developer.android.com/google/play/billing/integrate#dependency). ::: :::info 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-Flutter.svg?style=flat&logo=flutter)](https://github.com/adaptyteam/AdaptySDK-Flutter/releases) :::important Các bước dưới đây cài đặt SDK ổn định mới nhất (3.x). Nếu bạn cần v4 — bắt buộc cho [Flow Builder](adapty-flow-builder) và được sử dụng bởi [quickstart](flutter-quickstart-paywalls) — hãy làm theo [Adapty SDK 4.0: Swift Package Manager](#adapty-sdk-40-swift-package-manager) bên dưới. ::: 1. Thêm Adapty vào file `pubspec.yaml` của bạn: ```yaml showLineNumbers title="pubspec.yaml" dependencies: adapty_flutter: ^ ``` 2. Chạy lệnh sau để cài đặt các dependencies: ```bash showLineNumbers title="Terminal" flutter pub get ``` 3. Import Adapty SDK vào ứng dụng của bạn: ```dart showLineNumbers title="main.dart" import 'package:adapty_flutter/adapty_flutter.dart'; ``` ### Adapty SDK 4.0: Swift Package Manager Thêm Adapty Flutter SDK 4.0 — hỗ trợ [Flow Builder](adapty-flow-builder) — vào `pubspec.yaml` của bạn: ```yaml showLineNumbers title="pubspec.yaml" dependencies: adapty_flutter: 4.0.0 ``` Từ v4 trở đi, iOS SDK gốc không còn được phân phối qua CocoaPods nữa — plugin sẽ tải về thông qua **Swift Package Manager** ([kho spec của CocoaPods sẽ chuyển sang chỉ đọc vào tháng 12 năm 2026](https://blog.cocoapods.org/CocoaPods-Specs-Repo/)). Nếu bạn đang dùng Flutter 3.32–3.43, hãy bật hỗ trợ Swift Package Manager một lần: ```bash showLineNumbers title="Terminal" flutter config --enable-swift-package-manager ``` Flutter 3.44 trở lên đã bật Swift Package Manager theo mặc định, nên bạn không cần thực hiện thêm bước nào. Để xem các thay đổi API trong v4, tham khảo [hướng dẫn migration](migration-to-flutter-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 ứng dụng của bạn. :::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. ```dart showLineNumbers title="main.dart" void main() { runApp(MyApp()); } class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State { @override void initState() { _initializeAdapty(); super.initState(); } Future _initializeAdapty() async { try { await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY'), ); } catch (e) { // handle the error } } Widget build(BuildContext context) { return Text("Hello"); } } ``` :::important Hãy đợi `activate` hoàn tất 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 Flutter SDK](flutter-sdk-call-order) để biết toàn bộ trình tự. ::: Bây giờ hãy thiết lập paywall trong ứng dụng của bạn: - Nếu bạn sử 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 về Paywall Builder](flutter-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](flutter-quickstart-manual). ## Kích hoạt module AdaptyUI của Adapty SDK \{#activate-adaptyui-module-of-adapty-sdk\} Nếu bạn có kế hoạch sử dụng [Paywall Builder](adapty-paywall-builder) và đã [cài đặt module AdaptyUI](sdk-installation-flutter#install-adapty-sdk), bạn cũng cần kích hoạt AdaptyUI: :::note Các dependency liên quan đến AdaptyUI sẽ được liên kết với ứng dụng của bạn bất kể AdaptyUI có được kích hoạt hay không. ::: :::important Trong code của bạn, bạn phải kích hoạt module Adapty core trước khi kích hoạt AdaptyUI. ::: ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withActivateUI(true), // This automatically activates AdaptyUI ); ``` ## Thiết lập tùy chọn \{#optional-setup\} ### Ghi log \{#logging\} #### Thiết lập hệ thống ghi log \{#set-up-the-logging-system\} Adapty ghi lại các lỗi và thông tin quan trọng khác để giúp bạn hiểu điều gì đang xảy ra. Có các cấp độ sau: | Level | Mô tả | | :----------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- | | `AdaptyLogLevel.error` | Chỉ ghi lại các lỗi. | | `AdaptyLogLevel.warn` | Ghi lại 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ú ý. | | `AdaptyLogLevel.info` | Ghi lại các lỗi, cảnh báo và nhiều thông báo thông tin khác nhau. Giá trị mặc định. | | `AdaptyLogLevel.verbose` | Ghi lại mọi thông tin bổ sung có thể hữu ích trong quá trình debug, chẳng hạn như các lời gọi hàm, truy vấn API, v.v. | | `AdaptyLogLevel.debug` | Ghi lại thông tin debug. | Bạn có thể đặt mức độ log trong ứng dụng trước khi cấu hình Adapty: ```dart showLineNumbers title="main.dart" // Set log level before activation. // 'verbose' is recommended for development and the first production release await Adapty().setLogLevel(AdaptyLogLevel.verbose); // Or set it during configuration await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withLogLevel(AdaptyLogLevel.verbose), ); ``` ### 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 lên, nhưng bạn có thể áp dụng các chính sách bảo mật dữ liệu bổ sung để tuân thủ quy định của cửa hàng hoặc từng quốc gia. #### Tắt tính năng thu thập và chia sẻ địa chỉ IP \{#disable-ip-address-collection-and-sharing\} Khi khởi tạo module Adapty, đặt `ipAddressCollectionDisabled` thành `true` để tắt tính năng thu thập và chia sẻ địa chỉ IP của người dùng. Giá trị mặc định là `false`. Sử dụng tham số này để tăng cường quyền riêng tư cho 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 thiểu việc thu thập dữ liệu không cần thiết khi các tính năng dựa trên địa chỉ IP không được yêu cầu trong ứng dụng của bạn. ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withIpAddressCollectionDisabled(true), ); ``` #### Tắt tính năng thu thập và chia sẻ advertising ID \{#disable-advertising-id-collection-and-sharing\} Khi kích hoạt module Adapty, đặt `appleIdfaCollectionDisabled` (iOS) hoặc `googleAdvertisingIdCollectionDisabled` (Android) thành `true` để tắt việc thu thập mã định danh quảng cáo. Giá trị mặc định là `false`. Sử dụng tham số này để tuân thủ chính sách của App Store/Play Store, tránh kích hoạt lời nhắc App Tracking Transparency, hoặc nếu ứng dụng của bạn không cần attribution quảng cáo hay phân tích dựa trên ID quảng cáo. ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withAppleIdfaCollectionDisabled(true) // iOS ..withGoogleAdvertisingIdCollectionDisabled(true), // Android ); ``` #### Cấu hình bộ nhớ đệm media cho AdaptyUI \{#set-up-media-cache-configuration-for-adaptyui\} Module này được kích hoạt tự động cùng với Adapty SDK. Nếu bạn không sử dụng Paywall Builder và muốn tắt module AdaptyUI, hãy truyền `withActivateUI(false)` trong quá trình kích hoạt. Theo mặc định, AdaptyUI lưu bộ nhớ đệm cho các tệp media (như hình ảnh và video) để cải thiện hiệu suất và giảm lưu lượng mạng. Bạn có thể tùy chỉnh cài đặt bộ nhớ đệm bằng cách cung cấp một cấu hình tùy chỉnh. Sử dụng `withMediaCacheConfiguration` để ghi đè các giới hạn bộ nhớ đệm mặc định. Đây là tùy chọn — nếu bạn không gọi phương thức này, các giá trị mặc định sẽ được dùng (100MB dung lượng đĩa, không giới hạn số lượng trong bộ nhớ). Tuy nhiên, nếu bạn tạo đối tượng cấu hình, tất cả các tham số của nó đều là bắt buộc. ```dart showLineNumbers title="main.dart" final mediaCacheConfig = AdaptyUIMediaCacheConfiguration( memoryStorageTotalCostLimit: 200 * 1024 * 1024, // 200 MB memoryStorageCountLimit: 2147483647, // max int value diskStorageSizeLimit: 200 * 1024 * 1024, // 200 MB ); await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withMediaCacheConfiguration(mediaCacheConfig), ); ``` **Tham số:** | Tham số | Bắt buộc | Mô tả | |-------------------------|----------|-----------------------------------------------------------------------------| | memoryStorageTotalCostLimit | bắt buộc | Tổng kích thước cache trong bộ nhớ tính bằng byte. Mặc định là 100 MB. | | memoryStorageCountLimit | bắt buộc | Giới hạn số lượng item trong bộ nhớ. Mặc định là giá trị int tối đa. | | diskStorageSizeLimit | bắt buộc | Giới hạn kích thước file trên đĩa tính bằng byte. Mặc định là 100 MB. | ### Bật mức độ truy cập cục bộ (Android) \{#enable-local-access-levels-android\} Theo mặc định, [mức độ truy cập cục bộ](local-access-levels) được bật trên iOS và tắt trên Android. Để bật trên Android, hãy đặt `withGoogleLocalAccessLevelAllowed` thành `true`: ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withGoogleLocalAccessLevelAllowed(true), ); ``` ### Xóa dữ liệu khi khôi phục từ backup \{#clear-data-on-backup-restore\} Khi `appleClearDataOnBackup` được đặt thành `true`, SDK sẽ phát hiện khi ứng dụng được khôi phục từ bản 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 đã 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ỉ có bộ nhớ cache cục bộ của SDK bị xóa. Lịch sử giao dịch với Apple và dữ liệu người dùng trên máy chủ Adapty vẫn không thay đổi. ::: ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withAppleClearDataOnBackup(true) // default – false ); ``` ## Khắc phục sự cố \{#troubleshooting\} #### Quy tắc sao lưu Android (cấu hình Auto Backup) \{#android-backup-rules-auto-backup-configuration\} Một số SDK (bao gồm Adapty) đi kèm với cấu hình Android Auto Backup riêng. Nếu bạn sử dụng nhiều SDK có định nghĩa backup rules, quá trình merge Android manifest có thể thất bại với lỗi liên quan đến `android:fullBackupContent`, `android:dataExtractionRules`, hoặc `android:allowBackup`. Triệu chứng lỗi thường gặp: `Manifest merger failed: Attribute application@dataExtractionRules value=(@xml/your_data_extraction_rules) is also present at [com.other.sdk:library:1.0.0] value=(@xml/other_sdk_data_extraction_rules)` :::note Những thay đổi này cần được thực hiện trong thư mục platform Android của bạn (thường nằm trong thư mục `android/` của dự án). ::: Để khắc phục, bạn cần: - Yêu cầu manifest merger sử dụng các giá trị của ứng dụng cho các thuộc tính liên quan đến backup. - Tạo các file backup rule kết hợp rules của Adapty với rules từ các SDK khác. #### 1. Thêm namespace `tools` vào manifest \{#1-add-the-tools-namespace-to-your-manifest\} Trong file `AndroidManifest.xml`, hãy đảm bảo thẻ gốc `` có chứa tools: ```xml ... ``` #### 2. Ghi đè các thuộc tính backup trong `` \{#2-override-backup-attributes-in-application\} Trong cùng file `AndroidManifest.xml`, cập nhật thẻ `` để ứng dụng của bạn cung cấp các giá trị cuối cùng và yêu cầu manifest merger thay thế các giá trị từ thư viện: ```xml ... ``` Nếu có SDK nào cũng đặt `android:allowBackup`, hãy thêm nó vào `tools:replace`: ```xml tools:replace="android:allowBackup,android:fullBackupContent,android:dataExtractionRules" ``` #### 3. Tạo các file backup rules đã merge \{#3-create-merged-backup-rules-files\} Tạo các file XML trong thư mục `res/xml/` của dự án Android, kết hợp rules của Adapty với rules từ các SDK khác. Android sử dụng các định dạng backup rule khác nhau tùy theo phiên bản OS, vì vậy việc tạo cả hai file đảm bảo tương thích với tất cả các phiên bản Android mà ứng dụng hỗ trợ. :::note Các ví dụ dưới đây sử dụng AppsFlyer làm SDK bên thứ ba mẫu. Hãy thay thế hoặc bổ sung rules cho các SDK khác mà bạn đang dùng trong ứng dụng. ::: **Dành cho Android 12 trở lên** (sử dụng định dạng data extraction rules mới): ```xml title="sample_data_extraction_rules.xml" ``` **Dành cho Android 11 trở xuống** (sử dụng định dạng full backup content cũ): ```xml title="sample_backup_rules.xml" #### Giao dịch mua thất bại sau khi quay lại từ ứng dụng khác trên Android \{#purchases-fail-after-returning-from-another-app-in-android\} Nếu Activity khởi động luồng mua hàng sử dụng `launchMode` không phải mặc định, Android có thể tạo lại hoặc tái sử dụng Activity đó không đúng cách khi người dùng quay lại từ Google Play, ứng dụng ngân hàng, hoặc trình duyệt. Điều này có thể khiến kết quả giao dịch mua bị mất hoặc bị coi là đã hủy. Để đảm bảo giao dịch mua hoạt động đúng, chỉ sử dụng chế độ khởi chạy `standard` hoặc `singleTop` cho Activity khởi động luồng mua hàng, và tránh các chế độ khác. Trong `AndroidManifest.xml`, hãy đảm bảo Activity khởi chạy flow mua hàng được đặt thành `standard` hoặc `singleTop`: ```xml ``` #### Lỗi build Swift 6 do Podfile ghi đè SWIFT_VERSION \{#swift-6-build-errors-caused-by-podfile-swift_version-override\} Khi build ứng dụng Flutter cho iOS, bạn có thể gặp lỗi biên dịch Swift 6 trên các target pod của Adapty. Các triệu chứng thường gặp bao gồm: lỗi không khớp `@Sendable` trong `AdaptyUIBuilderLogic`, thiếu conformance `Sendable` trên các kiểu của Adapty, hoặc lỗi actor isolation. Pod Adapty khai báo `s.swift_version = '6.0'` và yêu cầu Swift 6 để build. Code của app bạn vẫn có thể dùng Swift 5 — chỉ các pod target của Adapty (`Adapty`, `AdaptyUI`, `AdaptyUIBuilder`, `AdaptyLogger`, `AdaptyPlugin`) mới cần build với Swift 6. Nguyên nhân phổ biến nhất là hook `post_install` trong `ios/Podfile` ghi đè `SWIFT_VERSION` cho tất cả các pod target: ```ruby showLineNumbers title="ios/Podfile" post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['SWIFT_VERSION'] = '5.9' end end end ``` **Cách khắc phục**: Loại trừ các pod target của Adapty khỏi phần ghi đè: ```ruby showLineNumbers title="ios/Podfile" post_install do |installer| installer.pods_project.targets.each do |target| next if %w[Adapty AdaptyUI AdaptyUIBuilder AdaptyLogger AdaptyPlugin].include?(target.name) target.build_configurations.each do |config| config.build_settings['SWIFT_VERSION'] = '5.9' end end end ``` Sau đó chạy `pod install` từ thư mục `ios/` và build lại. Để kiểm tra, mở `ios/Pods/Pods.xcodeproj`, chọn target pod `Adapty` → **Build Settings** → **Swift Language Version**. Giá trị phải là **Swift 6**. --- # File: flutter-quickstart-paywalls --- --- title: "Kích hoạt mua hàng với Flow Builder trong Flutter SDK" description: "Hướng dẫn nhanh để kích hoạt in-app purchase với Adapty Flow Builder." --- Để kích hoạt in-app purchase, bạn cần hiểu ba khái niệm chính: - [**Sản phẩm**](product) – những 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 hiển thị sản phẩm cho người dùng, được xây dựng bằng Flow Builder không cần code. SDK lấy chúng qua `getFlow`. Nếu bạn muốn tự xây dựng giao diện bằng code, hãy dùng paywall thay thế — xem [Triển khai paywall thủ công](flutter-quickstart-manual). - [**Placement**](placements) – vị trí và thời điểm hiển thị flow trong ứng dụng (ví dụ: `main`, `onboarding`, `settings`). Bạn gắn flow vào placement trên dashboard, sau đó yêu cầu chúng theo placement ID trong code. Cách này giúp bạn dễ dàng chạy A/B test và hiển thị các flow khác nhau cho từng nhóm người dùng. Adapty cung cấp cho bạn ba cách để kích hoạt tính năng mua hàng trong ứng dụng. Hãy chọn một trong số đó tùy theo yêu cầu của ứng dụng: | Triển khai | Độ phức tạp | Khi nào nên dùng | |------------------------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Adapty Flow Builder | ✅ Dễ | Bạn [tạo một flow hoàn chỉnh, sẵn sàng để mua hàng trong trình tạo không cần code](quickstart-paywalls). Adapty tự động hiển thị và xử lý toàn bộ luồng mua hàng phức tạp, xác thực biên lai và quản lý gói đăng ký. | | Paywall tự tạo | 🟡 Trung bình | Bạn tự xây dựng giao diện paywall trong code ứng dụng, nhưng vẫn lấy đối tượng flow từ Adapty để duy trì sự linh hoạt trong danh sách sản phẩm. Xem [hướng dẫn](flutter-quickstart-manual). | | Chế độ Observer | 🔴 Khó | Bạn đã có hạ tầng xử lý mua hàng riêng và muốn tiếp tục dùng nó. Lưu ý rằng chế độ observer có một số hạn chế trong Adapty. Xem [bài viết](observer-vs-full-mode). | :::important **Các bước dưới đây hướng dẫn cách triển khai một flow được tạo trong Adapty Flow Builder.** Nếu bạn muốn tự xây dựng giao diện paywall, hãy xem [Triển khai paywall thủ công](flutter-quickstart-manual). ::: Để hiển thị một flow được tạo trong Adapty Flow Builder, trong code của ứng dụng, bạn chỉ cần: 1. **Lấy flow**: Lấy flow đó từ Adapty. 2. **Hiển thị flow và Adapty sẽ xử lý các giao dịch mua cho bạn**: Hiển thị view trong ứng dụng của bạn. 3. **Xử lý các hành động nút bấm**: Liên kết các tương tác của người dùng với phản hồi tương ứng trong ứng dụng. Ví dụ: mở liên kết hoặc đóng flow khi người dùng nhấn nút. ## Trước khi bắt đầu \{#before-you-start\} Trước khi bắt đầu, hãy hoàn thành các bước sau: 1. Kết nối ứng dụng của bạn với [App Store](initial_ios) và/hoặc [Google Play](initial-android) 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-flutter) trong mã ứng dụng của bạn. Hướng dẫn này sử dụng API của Adapty Flutter SDK v4. :::tip Cách nhanh nhất để hoàn thành các bước này là làm theo [hướng dẫn quickstart](quickstart) hoặc tạo paywall và placement bằng [Developer CLI](developer-cli-quickstart). ::: ## 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 đã tạo trong Adapty Flow Builder, bạn cần: 1. Lấy đối tượng `flow` theo ID [placement](placements) bằng phương thức `getFlow` và kiểm tra xem nó có được tạo trong builder hay không thông qua thuộc tính `hasViewConfiguration`. 2. Tạo flow view bằng phương thức `createFlowView`. View chứa các phần tử UI và style cần thiết để hiển thị flow. :::important Để lấy cấu hình view, bạn phải bật toggle **Show on device** trong builder. Nếu không, bạn sẽ nhận được cấu hình view rỗng và flow sẽ không được hiển thị. ::: ```dart showLineNumbers try { // the requested flow final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID'); final view = await AdaptyUI().createFlowView( flow: flow, ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` ## 2. Hiển thị flow \{#display-the-flow\} Bây giờ, khi bạn đã có flow view, chỉ cần thêm vài dòng code để hiển thị nó. Để hiển thị flow, sử dụng phương thức `view.present()` trên `view` được tạo bởi phương thức `createFlowView`. Mỗi `view` chỉ có thể được hiển thị một lần: sau khi bạn đóng nó, view sẽ được giải phóng khỏi bộ nhớ. Nếu bạn cần hiển thị lại flow, hãy gọi `createFlowView` thêm một lần nữa để tạo một `view` instance mới. ```dart showLineNumbers title="Flutter" try { await view.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` :::tip Để biết thêm chi tiết về cách hiển thị một flow, hãy xem [hướng dẫn](flutter-present-paywalls) của chúng tôi. ::: ## 3. Xử lý các hành động nút bấm \{#handle-button-actions\} Khi người dùng nhấn các nút trong flow, Flutter SDK tự động xử lý các thao tác mua hàng, khôi phục, đóng màn hình và mở URL. Tuy nhiên, một số nút có ID tùy chỉnh hoặc được định sẵn và cần bạn xử lý trong code. Để kiểm soát hoặc theo dõi các tiến trình trên màn hình flow, hãy implement các phương thức của `AdaptyUIFlowsEventsObserver` và thiết lập observer trước khi hiển thị bất kỳ màn hình nào. Khi người dùng thực hiện một hành động, `flowViewDidPerformAction` sẽ được gọi, và ứng dụng của bạn cần phản hồi tương ứng với action ID đó. Có ba phương thức observer **bắt buộc**: `flowViewDidFinishPurchase`, `flowViewDidFinishRestore`, và `flowViewDidReceiveError` — class của bạn sẽ không thể biên dịch nếu thiếu chúng. :::tip Đọc hướng dẫn của chúng tôi về cách xử lý [actions](flutter-handle-paywall-actions) và [events](flutter-handling-events) của nút bấm. ::: Triển khai observer dưới dạng một đối tượng độc lập, tồn tại lâu dài thay vì gắn vào widget. Vì toàn bộ ứng dụng chỉ dùng chung một slot observer duy nhất, nếu gắn vào `State` sẽ gây rò rỉ màn hình (SDK giữ một strong reference đến nó) và sẽ bị thay thế âm thầm khi màn hình tiếp theo đăng ký. Sử dụng `extends` cũng kế thừa hành vi mặc định của SDK, vì vậy ngoài ba phương thức bắt buộc, bạn chỉ cần override những callback mà bạn quan tâm. ```dart showLineNumbers title="Flutter" // A dedicated, long-lived handler for flow events. // It does NOT live inside a Widget/State, so it never leaks and is never // silently replaced when screens are pushed or popped. class FlowEventsHandler extends AdaptyUIFlowsEventsObserver { // A single, app-wide instance — same idiom as Adapty() and AdaptyUI(). static final FlowEventsHandler _instance = FlowEventsHandler._(); factory FlowEventsHandler() => _instance; FlowEventsHandler._(); // This method is called when user performs an action on the flow UI. // Overriding it replaces the default behavior (dismiss on close, open URLs), // so keep those cases if you want to preserve it. @override void flowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIAction action) { switch (action) { case const CloseAction(): case const AndroidSystemBackAction(): // close the flow on the Android back button view.dismiss(); break; case OpenUrlAction(:final url, :final openIn): AdaptyUI().openUrl(url, openIn: openIn); break; default: break; } } // Required: decide what happens after a purchase finishes @override void flowViewDidFinishPurchase(AdaptyUIFlowView view, AdaptyPaywallProduct product, AdaptyPurchaseResult purchaseResult) { if (purchaseResult is! AdaptyPurchaseResultUserCancelled) { view.dismiss(); } } // Required: dismiss the flow once a restore succeeds @override void flowViewDidFinishRestore(AdaptyUIFlowView view, AdaptyProfile profile) { view.dismiss(); } // Required: handle rendering and other view errors @override void flowViewDidReceiveError(AdaptyUIFlowView view, AdaptyError error) { print('Flow error: $error'); view.dismiss(); } } ``` Đăng ký handler **một lần** khi khởi động ứng dụng, trước khi hiển thị bất kỳ flow nào: ```dart showLineNumbers title="Flutter" AdaptyUI().setFlowsEventsObserver(FlowEventsHandler()); ``` ## Bước tiếp theo \{#next-steps\} :::tip Bạn có câu hỏi hoặc gặp sự cố? Hãy xem [diễn đàn hỗ trợ](https://adapty.featurebase.app/) của chúng tôi — nơi bạn có thể tìm câu trả lời cho các câu hỏi thường gặp hoặc đặt câu hỏi của riêng mình. Đội ngũ và cộng đồng của chúng tôi luôn sẵn sàng giúp đỡ! ::: Paywall của bạn đã sẵn sàng để hiển thị trong ứng dụng. Hãy kiểm tra mua hàng trong [sandbox App Store](test-purchases-in-sandbox) hoặc [Google Play Store](testing-on-android) để đảm bảo bạn có thể hoàn thành một lần mua thử từ paywall. Tiếp theo, bạn cần [kiểm tra mức độ truy cập của người dùng](flutter-check-subscription-status) để đảm bảo bạn hiển thị paywall hoặc cấp quyền truy cập vào các tính năng trả phí cho đúng người dùng. ## Ví dụ đầy đủ \{#full-example\} Dưới đây là cách tích hợp tất cả các bước trên vào ứng dụng của bạn. ```dart void main() { // Register a single, long-lived observer once, before any flow is shown. // It is intentionally a plain object (NOT a Widget/State): its lifetime is the // whole app, so it never leaks and is never silently replaced when screens are // pushed or popped. AdaptyUI().setFlowsEventsObserver(FlowEventsHandler()); runApp(MaterialApp(home: FlowScreen())); } /// A dedicated handler for AdaptyUI flow events. /// /// It `extends` [AdaptyUIFlowsEventsObserver] (rather than being implemented /// by a `State`), which gives you two things for free: /// * the SDK's sensible defaults for optional callbacks, so besides the three /// required methods you only override what you actually care about; /// * a lifecycle that is independent of the widget tree — there is no strong /// reference back into a `Widget`, so nothing leaks and there is nothing to /// unregister. /// /// Every callback receives the [AdaptyUIFlowView] it relates to, so handling /// flow actions never requires a `BuildContext` or widget state. class FlowEventsHandler extends AdaptyUIFlowsEventsObserver { // A single, app-wide instance — same idiom as Adapty() and AdaptyUI(). static final FlowEventsHandler _instance = FlowEventsHandler._(); factory FlowEventsHandler() => _instance; FlowEventsHandler._(); // Called when the user performs an action on the flow UI. @override void flowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIAction action) { switch (action) { case const CloseAction(): case const AndroidSystemBackAction(): // close the flow on the Android back button view.dismiss(); break; case OpenUrlAction(:final url, :final openIn): // Open the URL natively, honoring the dashboard browser setting. AdaptyUI().openUrl(url, openIn: openIn); break; default: break; } } // Required: decide what happens after a purchase finishes. @override void flowViewDidFinishPurchase(AdaptyUIFlowView view, AdaptyPaywallProduct product, AdaptyPurchaseResult purchaseResult) { if (purchaseResult is! AdaptyPurchaseResultUserCancelled) { view.dismiss(); } } // Required: dismiss the flow once a restore succeeds. @override void flowViewDidFinishRestore(AdaptyUIFlowView view, AdaptyProfile profile) { view.dismiss(); } // Required: handle rendering and other view errors. @override void flowViewDidReceiveError(AdaptyUIFlowView view, AdaptyError error) { print('Flow error: $error'); view.dismiss(); } } class FlowScreen extends StatefulWidget { const FlowScreen({super.key}); @override State createState() => _FlowScreenState(); } class _FlowScreenState extends State { @override void initState() { super.initState(); _showFlowIfNeeded(); } Future _showFlowIfNeeded() async { try { final flow = await Adapty().getFlow( placementId: 'YOUR_PLACEMENT_ID', ); if (!flow.hasViewConfiguration) return; final view = await AdaptyUI().createFlowView(flow: flow); await view.present(); } catch (_) { // Handle any errors (network, SDK issues, etc.) } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Adapty Flow Example')), body: Center( // Add a button to re-trigger the flow for testing purposes. child: ElevatedButton( onPressed: _showFlowIfNeeded, child: const Text('Show Flow'), ), ), ); } } ``` --- # File: flutter-check-subscription-status --- --- title: "Kiểm tra trạng thái gói đăng ký trong Flutter SDK" description: "Tìm hiểu cách kiểm tra trạng thái gói đăng ký trong ứng dụng Flutter của bạn với Adapty." --- Để quyết định xem người dùng có thể truy cập nội dung trả phí hay cần xem 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ị gì cho họ — paywall hay nội dung trả phí. ## 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ơ của họ. Có hai lựa chọn: - Gọi `getProfile` khi 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** để giữ một 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. ### 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`: ```dart showLineNumbers try { final profile = await Adapty().getProfile(); // check the access } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ### Lắng nghe các cập nhật gói đăng ký \{#listen-to-subscription-updates\} Để tự động nhận cập nhật hồ sơ người dùng trong ứng dụng: 1. Sử dụng `Adapty().didUpdateProfileStream.listen()` để lắng nghe các thay đổi của hồ sơ người dùng — 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. 2. Lưu dữ liệu hồ sơ người dùng đã cập nhật khi phương thức này được gọi, để bạn có thể sử dụng trong toàn bộ ứng dụng mà không cần thực hiện thêm các yêu cầu mạng. ```dart class SubscriptionManager { AdaptyProfile? _currentProfile; SubscriptionManager() { // Listen for profile updates Adapty().didUpdateProfileStream.listen((profile) { _currentProfile = profile; // Update UI, unlock content, etc. }); } // Use stored profile instead of calling getProfile() bool hasAccess() { return _currentProfile?.accessLevels['premium']?.isActive ?? false; } } ``` :::note Adapty tự động gọi stream listener cập nhật hồ sơ người dùng khi ứng dụng khởi động, cung cấp dữ liệu gói đăng ký được lưu trong bộ nhớ 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 vào các tính năng trả phí, bạn có thể kiểm tra trực tiếp hồ sơ người dùng. Cách tiếp cận này hữu ích trong các tình huống như khi khởi chạy ứng dụng, khi người dùng truy cập vào các phần premium, hoặc trước khi hiển thị nội dung cụ thể. ```dart Future _checkAccessLevel() async { try { final profile = await Adapty().getProfile(); return profile.accessLevels['YOUR_ACCESS_LEVEL']?.isActive ?? false; } catch (e) { print('Error checking access level: $e'); return false; // Show paywall if access check fails } } Future _initializePaywall() async { await _loadPaywall(); final hasAccess = await _checkAccessLevel(); if (!hasAccess) { // Show paywall if no access } } ``` ## Bước tiếp theo \{#next-steps\} Sau khi đã 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](flutter-quickstart-identify) để đảm bảo họ có thể truy cập những gì đã thanh toán. --- # File: flutter-quickstart-identify --- --- title: "Xác định người dùng trong Flutter SDK" description: "Hướng dẫn nhanh để thiết lập Adapty cho quản lý gói đăng ký trong ứng dụng với Flutter." --- :::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. Tại đâ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ó tương thích với hệ thống xác thực hiện tại 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ơ** 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 các hồ sơ trong Adapty với hệ thống xác thực nội bộ của bạn. Đâ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ý giao dịch mua** | Khôi phục giao dịch mua ở cấp độ cửa hàng | Duy trì lịch sử giao dịch mua trên nhiều thiết bị thông qua customer user ID | | **Quản lý hồ sơ** | Hồ sơ mới khi cài đặt lại | Cùng một hồ sơ trên nhiều phiên và thiết bị | | **Lưu trữ dữ liệu** | Dữ liệu người dùng ẩn danh gắn với lần cài đặt ứng dụng | Dữ liệu người dùng đã xác định tồn tại qua 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 ứng dụng khởi chạy, Adapty **tạo một hồ sơ mới cho người dùng**. 2. Khi người dùng mua bất kỳ 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 trên **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 đã từng mua hàng trong ứng dụng trước đó, theo mặc định, các giao dịch mua của họ sẽ tự động được đồng bộ từ App Store khi SDK được kích hoạt. Vì vậy, với người dùng ẩn danh, các hồ sơ mới sẽ được tạo sau mỗi lần cài đặt, nhưng điều đó không phải là vấn đề vì trong Adapty analytics, 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). Với người dùng ẩn danh, bạn cần đếm số lần cài đặt theo **device ID**. Trong trường hợp này, mỗi lần cài đặt ứng dụng trên 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, hãy 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 chạy, 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ẻ, do đó 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 chạy (ví dụ: sau khi họ đăng nhập hoặc đăng ký), hãy sử dụng phương thức `identify` để thiết lập 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 người. ::: 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ẽ trỏ đến hồ sơ ẩn danh. Xem [Thứ tự gọi trong Flutter SDK](flutter-sdk-call-order). ```dart showLineNumbers try { await Adapty().identify(customerUserId); // Unique for each user } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ### 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ỉ thiết lập 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 một customer user ID hiện có (cái bạn đã sử dụng trước đây) hoặc một cái mới. Nếu bạn truyền một cái mới, hồ sơ mới đượ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 các dashboard analytics, vì số lần cài đặt được tính dựa trên device ID. Một device ID đại diện cho một lần cài đặt ứng dụng từ cửa hàng trên 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ạ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 thêm sự kiện cài đặt. Nếu bạn muốn đếm số 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). ::: ```dart showLineNumbers" try { await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_API_KEY') ..withCustomerUserId(YOUR_CUSTOMER_USER_ID) // Customer user IDs must be unique for each user. If you hardcode the parameter value, all users will be considered as one. ); } catch (e) { // 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 đó. ::: ```dart showLineNumbers try { await Adapty().logout(); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle unknown error } ``` :::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ọ sẽ 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ử giao dịch mua đượ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 hồ sơ. Bạn có thể gọi [`getProfile`](flutter-check-subscription-status) ngay sau khi xác định, hoặc [lắng nghe các cập nhật hồ sơ](flutter-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! Để tận dụng Adapty nhiều hơn, bạn có thể khám phá các chủ đề sau: - [**Kiểm thử**](troubleshooting-test-purchases): Đảm bảo mọi thứ hoạt động như mong đợi - [**Onboarding**](flutter-onboardings): Thu hút người dùng bằng onboarding và tăng tỷ lệ giữ chân - [**Tích hợp**](configuration): Tích hợp với các dịch vụ attribution marketing và analytics chỉ trong một dòng code - [**Thiết lập thuộc tính hồ sơ tùy chỉnh**](flutter-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, giúp bạn có thể chạy A/B test hoặc hiển thị các paywall khác nhau cho các nhóm người dùng khác nhau --- # File: adapty-sdk-integration-skill-flutter --- --- title: "Tích hợp Adapty vào ứng dụng Flutter 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 Flutter của bạn từ đầu đến cuối với công cụ lập trình AI." --- :::important Kỹ năng này đang trong 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-flutter) — hướng dẫn này sẽ dẫn dắt công cụ AI của bạn qua từng giai đoạn với tài liệu phù hợp. ::: [Adapty-sdk-integration skill](https://github.com/adaptyteam/adapty-sdk-integration-skill) tự động hóa quá trình tích hợp Adapty từ đầu đến cuối: thiết lập dashboard, cài đặt SDK, paywall và xác minh từng bước. Skill này tự động phát hiện nền tảng của bạn và lấy 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 form 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** — sử dụng [skills CLI](https://skills.sh) (lưu ý rằng các skill được cài đặt theo cách này sẽ không tự động cập nhật): ``` npx skills add adaptyteam/adapty-sdk-integration-skill ``` Ngoài ra, bạn có thể clone repo và sao chép `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 xong, 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-flutter --- --- title: "Tích hợp Adapty vào ứng dụng Flutter của bạn 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 Flutter của bạn bằng Cursor, Context7, ChatGPT, Claude hoặc các công cụ AI khác." --- Hướng dẫn này sẽ giúp bạn tích hợp Adapty vào ứng dụng Flutter từng bước với công cụ lập trình AI — bạn cung cấp cho nó đú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 bằng một LLM skill tương tác, hoặc thủ công thông qua Dashboard. ### Cách dùng Skill (khuyến nghị) \{#skill-approach-recommended\} Adapty CLI skill cho phép LLM của bạn thiết lập app, sản phẩm, mức độ truy cập, paywall và placement trực tiếp — không cần mở Dashboard cho từng bước. Bạn chỉ cần [kết nối các cửa hàng](integrate-payments) trong Dashboard. ``` npx skills add adaptyteam/adapty-cli --skill adapty-cli ``` Sau khi thêm skill, chạy `/adapty-cli` trong agent của bạn. Nó sẽ hướng dẫn bạn qua từng bước — bao gồm cả lúc cần mở Dashboard để kết nối các cửa hàng. ### Cách thiết lập thủ công trên Dashboard \{#dashboard-approach\} Nếu bạn muốn tự cấu hình mọi thứ, đây là những gì cần có trước khi viết code. LLM của bạn không thể tra cứu các giá trị trên dashboard — bạn sẽ cần tự cung cấp chúng. 1. **Kết nối các cửa hàng ứng dụng**: Trong Adapty Dashboard, vào **App settings → General**. Kết nối cả App Store và Google Play nếu ứng dụng Flutter của bạn hỗ trợ cả hai nền tảng. Đây là điều bắt buộc để các giao dịch mua hoạt động. [Kết nối cửa hàng ứng dụng](integrate-payments) 2. **Sao chép Public SDK key**: 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 cấu hình Adapty. 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 sản phẩm trực tiếp trong code — Adapty cung cấp chúng thông qua paywall. [Thêm sản phẩm](quickstart-products) 4. **Tạo một paywall và một placement**: Trong Adapty Dashboard, tạo paywall trên trang **Paywalls**, sau đó gán nó vào một placement trên trang **Placements**. Trong code, placement ID là chuỗi bạn truyền vào `Adapty().getPaywall()`. [Tạo paywall](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']?.isActive`. Mức độ truy cập `premium` mặc định phù hợp với hầu hết các ứng dụng. Nếu người dùng trả phí có quyền 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 thêm mức độ truy cập](assigning-access-level-to-a-product) trước khi bắt đầu viết code. :::tip Khi đã có đủ năm điều trên, 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 viết code, 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 paywall và placement**: Thêm các lời gọi `getPaywall` với các placement ID khác nhau. - **Tích hợp analytics**: Cấu hình trên trang **Integrations**. Cách 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\} ### Dùng Context7 (khuyến nghị) \{#use-context7-recommended\} [Context7](https://context7.com) là một MCP server 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 luôn 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 Context7 server. Để thiết lập thủ công, xem [kho GitHub Context7](https://github.com/upstash/context7). Sau khi cấu hình, 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 Flutter SDK ``` :::warning Dù Context7 không còn 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. ::: ### Dùng tài liệu dạng plain text \{#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 Markdown thuần túy. Thêm `.md` vào cuối URL, hoặc nhấn **Copy for LLM** bên dưới tiêu đề bài viết. Ví dụ: [adapty-cursor-flutter.md](https://adapty.io/docs/vi/adapty-cursor-flutter.md). Mỗi bước trong [hướng dẫn triển khai](#implementation-walkthrough) bên dưới đều có khối "Gửi cho LLM của bạn" với các link `.md` để dán vào. Để xem thêm tài liệu cùng lúc, xem [các file index và tập hợp 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 sẽ đi qua việc tích hợp Adapty theo thứ tự triển khai. Mỗi bước bao gồm tài liệu cần gửi cho LLM, những gì bạn cần 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 bất kỳ code nào. Hãy cho LLM biết cách bạn xử lý giao dịch mua — điều này ảnh hưởng đến các hướng dẫn mà nó cần theo dõi: - [**Adapty Paywall Builder**](adapty-paywall-builder): Bạn tạo paywall trong trình xây dựng không cần code của Adapty, và SDK tự động hiển thị chúng. - [**Paywall tự tạo**](flutter-making-purchases): 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ý giao dịch mua. - [**Observer mode**](observer-vs-full-mode): Bạn giữ nguyên 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](flutter-quickstart-paywalls). ### Cài đặt và cấu hình SDK \{#install-and-configure-the-sdk\} Thêm dependency Adapty SDK bằng `flutter pub add` 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ì khác 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-flutter) Gửi nội dung này cho LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/sdk-installation-flutter.md ``` :::tip[Checkpoint] - **Kết quả mong đợi:** Ứng dụng build và chạy được trên cả iOS và Android. Console debug hiển thị log kích hoạt Adapty. - **Lưu ý:** "Public API key is missing" → kiểm tra xem bạn đã thay thế placeholder bằng key thực của mình từ App settings chưa. ::: ### Hiển thị paywall và xử lý giao dịch mua \{#show-paywalls-and-handle-purchases\} Lấy 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ý giao dịch mua. Kiểm thử từng giao dịch mua trong sandbox khi bạn làm — đừng đợi đến cuối. Xem [Kiểm thử giao dịch mua trong sandbox](test-purchases-in-sandbox) để biết hướng dẫn thiết lập. **Hướng dẫn:** - [Kích hoạt giao dịch mua bằng paywall (quickstart)](flutter-quickstart-paywalls) - [Lấy paywall từ Paywall Builder và cấu hình của chúng](flutter-get-pb-paywalls) - [Hiển thị paywall](flutter-present-paywalls) - [Xử lý sự kiện paywall](flutter-handling-events) - [Phản hồi các hành động nút](flutter-handle-paywall-actions) Gửi nội dung này cho LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/flutter-quickstart-paywalls.md - https://adapty.io/docs/vi/flutter-get-pb-paywalls.md - https://adapty.io/docs/vi/flutter-present-paywalls.md - https://adapty.io/docs/vi/flutter-handling-events.md - https://adapty.io/docs/vi/flutter-handle-paywall-actions.md ``` :::tip[Checkpoint] - **Kết quả mong đợi:** Paywall hiển thị với các sản phẩm bạn đã cấu hình. Nhấn vào sản phẩm kích hoạt hộp thoại mua hàng sandbox. - **Lưu ý:** Paywall trống hoặc lỗi `getPaywall` → xác nhận 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 giao dịch mua trong paywall tùy chỉnh của bạn (quickstart)](flutter-quickstart-manual) - [Lấy paywall và sản phẩm](fetch-paywalls-and-products-flutter) - [Hiển thị paywall được thiết kế bằng Remote Config](present-remote-config-paywalls-flutter) - [Thực hiện giao dịch mua](flutter-making-purchases) - [Khôi phục giao dịch mua](flutter-restore-purchase) Gửi nội dung này cho LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/flutter-quickstart-manual.md - https://adapty.io/docs/vi/fetch-paywalls-and-products-flutter.md - https://adapty.io/docs/vi/present-remote-config-paywalls-flutter.md - https://adapty.io/docs/vi/flutter-making-purchases.md - https://adapty.io/docs/vi/flutter-restore-purchase.md ``` :::tip[Checkpoint] - **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 sản phẩm kích hoạt hộp thoại mua hàng sandbox. - **Lưu ý:** Mảng sản phẩm trống → xác nhận 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-flutter) - [Báo cáo giao dịch trong Observer mode](report-transactions-observer-mode-flutter) Gửi nội dung này cho 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-flutter.md - https://adapty.io/docs/vi/report-transactions-observer-mode-flutter.md ``` :::tip[Checkpoint] - **Kết quả mong đợi:** Sau một giao dịch mua 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 nhận bạn đang báo cáo giao dịch cho Adapty và thông báo server đã được cấu hình cho cả hai cửa hàng. ::: ### Kiểm tra trạng thái gói đăng ký \{#check-subscription-status\} Sau khi mua, kiểm tra hồ sơ người dùng để xác nhận mức độ truy cập đang hoạt động nhằm chặn nội dung premium. **Hướng dẫn:** [Kiểm tra trạng thái gói đăng ký](flutter-check-subscription-status) Gửi nội dung này cho LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/flutter-check-subscription-status.md ``` :::tip[Checkpoint] - **Kết quả mong đợi:** Sau giao dịch mua sandbox, `profile.accessLevels['premium']?.isActive` trả về `true`. - **Lưu ý:** `accessLevels` trố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ơ người dùng Adapty để giao dịch mua đượ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](flutter-quickstart-identify) Gửi nội dung này cho LLM của bạn: ``` Read these Adapty docs before writing code: - https://adapty.io/docs/vi/flutter-quickstart-identify.md ``` :::tip[Checkpoint] - **Kết quả mong đợi:** Sau khi gọi `Adapty().identify()`, phần **Profiles** trên dashboard hiển thị custom user ID 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 tốt trong sandbox, hãy đi qua danh sách kiểm tra phát hành để đảm bảo mọi thứ đã sẵn sàng cho môi trường sản xuất. **Hướng dẫn:** [Danh sách kiểm tra phát hành](release-checklist) Gửi nội dung này cho LLM của bạn: ``` Read these Adapty docs before releasing: - https://adapty.io/docs/vi/release-checklist.md ``` :::tip[Checkpoint] - **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 server, flow mua hàng, kiểm tra mức độ truy cập và yêu cầu quyền riêng tư. - **Lưu ý:** Thiếu thông báo server → cấu hình App Store Server Notifications trong **App settings → iOS SDK** và Google Play Real-Time Developer Notifications trong **App settings → Android SDK**. ::: ## Các file index tài liệu dạng plain text \{#plain-text-doc-index-files\} Nếu bạn cần cung cấp cho LLM nhiều ngữ cảnh hơn ngoài các trang riêng lẻ, chúng tôi cung cấp các file index liệt kê hoặc tổng 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 link `.md`. Đây là [tiêu chuẩn đang nổi lên](https://llmstxt.org/) để làm cho các website dễ tiếp cận với LLM. Lưu ý rằng với một số AI agent (ví dụ: ChatGPT), bạn sẽ cần tải `llms.txt` về và tải lên chat dưới dạng file. - [`llms-full.txt`](https://adapty.io/docs/vi/llms-full.txt): Toàn bộ trang tài liệu Adapty được gộp vào một file duy nhất. Rất lớn — chỉ dùng khi bạn cần toàn bộ thông tin. - Tập hợp dành riêng cho Flutter [`flutter-llms.txt`](https://adapty.io/docs/vi/flutter-llms.txt) và [`flutter-llms-full.txt`](https://adapty.io/docs/vi/flutter-llms-full.txt): Tập hợp theo nền tảng giúp tiết kiệm token so với toàn bộ site. --- # File: flutter-get-pb-paywalls --- --- title: "Lấy flows & paywalls - Flutter" description: "Lấy flows và paywalls từ Adapty trong ứng dụng Flutter của bạn." --- Sau khi [bạn đã thiết kế flow hoặc paywall bằng 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 liên kết với placement cùng cấu hình view như mô tả bên dưới. Lưu ý rằng chủ đề này đề cập đến các flow và 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, vui lòng tham khảo chủ đề [Lấy paywall và sản phẩm cho paywall remote config trong ứng dụng di động của bạn](fetch-paywalls-and-products-flutter). :::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 hiển thị flow và paywall 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/paywall và thêm sản phẩm vào đó](create-paywall) trong Adapty Dashboard. 3. [Tạo placement và thêm flow/paywall vào đó](create-placement) trong Adapty Dashboard. 4. Cài đặt [Adapty SDK](sdk-installation-flutter) vào ứng dụng mobile của bạn.
## Lấy 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 để 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 hiển thị nó trong ứng dụng. Tải flow hoặc paywall và tạo [view](flutter-get-pb-paywalls#fetch-the-view-configuration) cho nó càng sớm càng tốt — lý tưởng nhất là trước khi hiển thị. Phương thức `createFlowView` tải cấu hình view và bắt đầu tải xuống, lưu cache hình ảnh ở nền. Bạn gọi nó càng sớm thì hình ảnh càng có nhiều thời gian để tải xong. Đến lúc 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 hiển thị. Để lấy flow hoặc paywall, dùng phương thức `getFlow`: ```dart showLineNumbers try { final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID'); // the requested flow/paywall } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` Các 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. | | **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 người dùng của bạn thường xuyên gặp vấn đề về kết nối internet, 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 tuyệt đối, nhưng thời gian tải sẽ nhanh hơn bất kể kết nối internet của họ như thế nào. Cache đượ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 để 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 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 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 |

Một `Duration` 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`, 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 \{#response-parameters\} | Tham số | Mô tả | | :-------- |:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Flow | Một đối tượng `AdaptyFlow` chứa các định danh của flow (`instanceIdentity`, `variationId`), tên, placement, các biến thể paywall (`paywalls`), và các Remote Config (`remoteConfigs`). | ## Lấy cấu hình view \{#fetch-the-view-configuration\} :::important Hãy đảm bảo bạn đã bật toggle **Show on device** trong builder. Nếu tùy chọn này chưa được bật, cấu hình view sẽ không thể truy xuất được. ::: Nếu placement được thiết kế trong **Flow Builder** hoặc **Paywall Builder**, Adapty sẽ tự động render giao diện — thuộc tính `hasViewConfiguration` của flow được tải về sẽ là `true`. Tạo view bằng `createFlowView`, sau đó [hiển thị flow hoặc paywall](flutter-present-paywalls). Nếu placement là một paywall tùy chỉnh không có giao diện Builder (`hasViewConfiguration` là `false`), hãy [xử lý nó như một remote config paywall](present-remote-config-paywalls-flutter). :::warning Kết quả của phương thức `createFlowView` chỉ có thể được hiển thị một lần. Nếu bạn cần hiển thị lại, hãy gọi lại phương thức `createFlowView` từ đầu. ::: ```dart showLineNumbers try { final view = await AdaptyUI().createFlowView(flow: flow); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` Tham số: | Tham số | Bắt buộc | Mô tả | | :------------------- | :------- | :----------------------------------------------------------- | | **flow** | bắt buộc | Một đối tượng `AdaptyFlow` để lấy view cho flow/paywall mong muốn. | | **customTags** | tùy chọn | Định nghĩa một map các custom tag và giá trị tương ứng. Custom tag đóng vai trò là placeholder trong nội dung, được thay thế động bằng các chuỗi cụ thể để cá nhân hóa nội dung trong flow/paywall. Xem thêm tại chủ đề [Custom tags in paywall builder](custom-tags-in-paywall-builder). | | **preloadProducts** | tùy chọn | Bật tùy chọn này để tối ưu thời điểm hiển thị sản phẩm trên màn hình. Khi đặt là `true`, AdaptyUI sẽ tự động tải các sản phẩm cần thiết. Mặc định: `false`. | | **loadTimeout** | tùy chọn | Một `Duration` giới hạn thời gian tải cấu hình view. Nếu hết thời gian chờ, dữ liệu đã cache hoặc fallback cục bộ sẽ được sử dụng. | :::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 flow](add-paywall-locale-in-adapty-paywall-builder) và cách sử dụng mã locale đúng cách [tại đây](flutter-localizations-and-locale-codes). ::: Sau khi có view, hãy [hiển thị flow/paywall](flutter-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ó 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ị 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ể sử dụng phương thức `getFlowForDefaultAudience`, phương thức này sẽ 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ư được mô tả chi tiết trong phần [Lấy flow/paywall](#fetch-flowpaywall) ở 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 đề 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 (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 (legacy) hoặc chấp nhận rằng người dùng có phiên bản hiện tại (legacy) 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 đi 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 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](#fetch-flowpaywall). ::: ```dart showLineNumbers try { final flow = await Adapty().getFlowForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID'); // the requested flow/paywall } on AdaptyError catch (adaptyError) { // handle error } catch (e) { // handle unknown 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ị 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 người dùng của bạn thường 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 dù kết nối mạng chập chờn. 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 request 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 xóa thủ công.

| ## Tùy chỉnh asset \{#customize-assets\} Để tùy chỉnh hình ảnh và video trong flow/paywall của bạn, hãy triển khai custom asset. 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 đế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 custom ID](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. Here's an example of how you can provide custom assets via a simple dictionary: ```dart final customAssets = { // Show a local image using a custom ID 'custom_image': AdaptyCustomAsset.localImageAsset( assetId: 'assets/images/image_name.png', ), // Show a local video with a preview image 'hero_video': AdaptyCustomAsset.localVideoAsset( assetId: 'assets/videos/custom_video.mp4', ), }; try { final view = await AdaptyUI().createFlowView( flow: flow, customAssets: customAssets, ); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` :::note Nếu không tìm thấy tài nguyên, flow/paywall sẽ trở về giao diện mặc định của nó. ::: ## Cài đặt timer tùy chỉnh do developer định nghĩa \{#set-up-developer-defined-timers\} Để sử dụng timer tùy chỉnh trong ứng dụng di động của bạn, hãy truyền một map `customTimers` vào phương thức `createFlowView`. Mỗi key trong map là một timer ID, và giá trị tương ứng là một đối tượng `DateTime` xác định thời điểm timer kết thúc. Dưới đây là ví dụ: ```dart showLineNumbers try { final view = await AdaptyUI().createFlowView( flow: flow, customTimers: { 'CUSTOM_TIMER_6H': DateTime.now().add(const Duration(seconds: 3600 * 6)), 'CUSTOM_TIMER_NY': DateTime(2027, 1, 1), // New Year 2027 }, ); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` Trong ví dụ này, `CUSTOM_TIMER_NY` và `CUSTOM_TIMER_6H` là **Timer ID** của các timer do developer định nghĩa mà bạn đã thiết lập trong Adapty Dashboard. Map `customTimers` đả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 Năm Mới. - `CUSTOM_TIMER_6H`: Thời gian còn lại trong khoảng thời gian 6 giờ bắt đầu khi người dùng mở flow.
Sau khi [bạn đã thiết kế phần giao diện cho paywall của mình](adapty-paywall-builder) bằng Paywall Builder mới trong Adapty Dashboard, bạn có thể hiển thị nó trong ứng dụng di động. Bước đầu tiên trong quá trình này là lấy paywall được liên kết với placement và cấu hình view của nó như mô tả bên dưới. :::warning Paywall Builder mới hoạt động với Flutter SDK phiên bản 3.3.0 trở lên. ::: Trước khi bắt đầu hiển thị paywall trong ứng dụng của bạn, hãy 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 thủ công, hãy tham khảo chủ đề [Lấy paywall và sản phẩm cho paywall Remote Config trong ứng dụng mobile](fetch-paywalls-and-products-flutter). :::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 hiển thị paywall 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 paywall và tích hợp các sản phẩm vào đó](create-paywall) trong Adapty Dashboard. 3. [Tạo placement và tích hợp paywall vào đó](create-placement) trong Adapty Dashboard. 4. Cài đặt [Adapty SDK](sdk-installation-flutter) vào ứng dụng di động của bạn.
## Lấy 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 hiển thị. Tuy nhiên, bạn vẫn cần lấy ID của nó qua placement, cấu hình view, rồi mới hiển thị trong ứng dụng. Để đảm bảo hiệu suất tối ưu, điều quan trọng là phải lấy paywall và [cấu hình view](flutter-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder) của nó càng sớm càng tốt, cho phép đủ thời gian để tải hình ảnh trước khi hiển thị cho người dùng. Để lấy paywall, sử dụng phương thức `getPaywall`: ```dart showLineNumbers try { final paywall = await Adapty().getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en"); // the requested paywall } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` 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 địa hóa paywall](add-paywall-locale-in-adapty-paywall-builder). Tham số này được kỳ vọng là mã ngôn ngữ bao gồm một hoặc hai thẻ phụ được phân tách bằng ký tự dấu trừ (**-**). Thẻ phụ đầu tiên là ngôn ngữ, thẻ phụ thứ hai là khu vực.

Ví dụ: `en` nghĩa là tiếng Anh, `pt-br` đại diện cho tiếng Bồ Đào Nha Brazil.

Xem [Bản địa hóa và mã locale](flutter-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 đã 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 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ẽ có thời gian tải nhanh hơn, bất kể kết nối internet của họ có chập chờn đến đâu. Cache được cập nhật thường xuyên, vì vậy việc sử dụng 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òn 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ữ 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 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ờ của 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`, vì thao tác có thể bao gồm nhiều yêu cầu khác nhau bên dưới.

Đối với Android: Bạn có thể tạo `TimeInterval` bằng các hàm mở rộng (như `5.seconds`, trong đó `.seconds` là từ `import com.adapty.utils.seconds`), hoặc `TimeInterval.seconds(5)`. Để không giới hạn, sử dụng `TimeInterval.INFINITE`.

| ## Tham số phản hồi \{#response-parameters\} | Tham số | Mô tả | | :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | Một đối tượng [`AdaptyPaywall`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html) chứa danh sách ID sản phẩm, 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 nút **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ể truy xuất được. ::: Sau khi tải paywall về, hãy kiểm tra xem nó có chứa `ViewConfiguration` hay không — đây là dấu hiệu cho biết paywall được tạo bằng Paywall Builder. Điều này sẽ giúp bạn xác định cách hiển thị paywall. Nếu `ViewConfiguration` có mặt, hãy xử lý nó như paywall Paywall Builder; nếu không, [xử lý nó như paywall Remote Config](present-remote-config-paywalls-flutter). ```dart showLineNumbers try { final view = await AdaptyUI().createPaywallView( paywall: paywall, ); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` Sau khi có view, hãy [hiển thị paywall](flutter-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 về 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à paywall, và người dùng của bạn có 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 như vậy, 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ị paywall nào cả. Để giải quyết vấn đề này, bạn có thể dùng phương thức `getPaywallForDefaultAudience`, phương thức này sẽ lấy paywall của placement được 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ư đã trình bày trong phần [Lấy thông tin paywall](flutter-get-pb-paywalls#fetch-paywall-designed-with-paywall-builder) ở trên. :::warning Tại sao chúng tôi khuyến nghị dùng `getPaywall` Phương thức `getPaywallForDefaultAudience` có một số hạn chế đáng kể: - **Các 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 có 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 nhắm 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 chấp nhận những hạn chế này để đổi lấy 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 tiếp tục dùng `getPaywall` như đã mô tả [ở trên](#fetch-paywall-designed-with-paywall-builder). ::: ```dart showLineNumbers try { final paywall = await Adapty().getPaywallForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID'); } on AdaptyError catch (adaptyError) { // handle error } catch (e) { // handle unknown error } ``` :::note Phương thức `getPaywallForDefaultAudience` khả dụng từ Flutter SDK phiên bản 3.2.0 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 là mã ngôn ngữ gồm một hoặc nhiều thẻ con được phân tách bằng ký tự gạch nối (**-**). 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 của 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 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 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ể kết nối internet của họ kém đến đâu. Cache được cập nhật thường xuyên, vì vậy có thể dùng an toàn 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 thực hiện dọn dẹp 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 nghĩa 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 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 custom ID](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ị 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. :::important Để sử dụng tính năng này, hãy cập nhật Adapty Flutter SDK lên phiên bản 3.8.0 trở lên. ::: Dưới đâ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: ```dart final customAssets = { // Show a local image using a custom ID 'custom_image': AdaptyCustomAsset.localImageAsset( assetId: 'assets/images/image_name.png', ), // Show a local video with a preview image 'hero_video': AdaptyCustomAsset.localVideoAsset( assetId: 'assets/videos/custom_video.mp4', ), }; try { final view = await AdaptyUI().createPaywallView( paywall: paywall, customAssets: customAssets, ); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` :::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 nhà phát triển định nghĩa \{#set-up-developer-defined-timers\} Để sử dụng custom timer trong ứng dụng di động, hãy truyền một map `customTimers` vào phương thức `createPaywallView`. Mỗi key trong map là một timer ID, và giá trị tương ứng là một đối tượng `DateTime` xác định thời điểm timer kết thúc. Dưới đây là ví dụ: ```dart showLineNumbers try { final view = await AdaptyUI().createPaywallView( paywall: paywall, customTimers: { 'CUSTOM_TIMER_6H': DateTime.now().add(const Duration(seconds: 3600 * 6)), 'CUSTOM_TIMER_NY': DateTime(2025, 1, 1), // New Year 2025 }, ); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` Trong ví dụ này, `CUSTOM_TIMER_NY` và `CUSTOM_TIMER_6H` là các **Timer ID** của timer do developer định nghĩa, được cài đặt trong Adapty Dashboard. Map `customTimers` đả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 Năm Mới. - `CUSTOM_TIMER_6H`: Thời gian còn lại trong khoảng thời gian 6 giờ bắt đầu khi người dùng mở paywall.
--- # File: flutter-present-paywalls --- --- title: "Hiển thị flows & paywalls - Flutter" description: "Hiển thị flows và paywalls trong ứng dụng Flutter sử dụng các tính năng monetization của Adapty." --- 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 để hiển thị cho người dùng. Flow hoặc paywall như vậy đã chứa cả nội dung cần hiển thị lẫn cách hiển thị nó. :::warning Hướng dẫn này dành cho flow và paywall được tạo bằng Paywall Builder. Để hiển thị **paywall Remote Config**, xem [Render paywall được thiết kế bằng Remote Config](present-remote-config-paywalls-flutter). ::: Adapty Flutter SDK cung cấp hai cách để hiển thị flow và paywall: - **Màn hình độc lập** - **Widget nhúng** ## Hiển thị dưới dạng màn hình độc lập \{#present-as-standalone-screen\} Để hiển thị một flow hoặc paywall dưới dạng màn hình độc lập, hãy dùng phương thức `view.present()` trên `view` được tạo bởi phương thức [`createFlowView`](flutter-get-pb-paywalls#fetch-the-view-configuration). Mỗi `view` chỉ có thể được hiển thị một lần: sau khi bạn đóng nó, view sẽ được giải phóng khỏi bộ nhớ. Nếu cần hiển thị lại flow hoặc paywall, hãy gọi lại `createFlowView` một lần nữa để tạo một `view` mới. ```dart showLineNumbers title="Flutter" try { await view.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // 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. ::: ### Đóng flow hoặc paywall \{#dismiss-the-flow-or-paywall\} Khi cần đóng flow hoặc paywall theo cách lập trình, hãy dùng phương thức `dismiss()`: ```dart showLineNumbers title="Flutter" try { await view.dismiss(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` :::note Đóng một view sẽ giải phóng nó khỏi bộ nhớ — view đã đóng không thể hiển thị lại. Hãy tạo view mới bằng `createFlowView` thay thế. ::: ### Hiển thị hộp thoại \{#show-dialog\} Dùng phương thức này thay cho các hộp thoại alert thông thường khi hiển thị flow hoặc paywall view trên Android. Trên Android, các alert thông thường sẽ xuất hiện phía sau view, khiến người dùng không nhìn thấy. Phương thức này đảm bảo hộp thoại luôn hiển thị đúng phía trên flow hoặc paywall trên mọi nền tảng. ```dart showLineNumbers title="Flutter" try { final action = await view.showDialog( title: 'Close paywall?', content: 'You will lose access to exclusive offers.', primaryActionTitle: 'Stay', secondaryActionTitle: 'Close', ); if (action == AdaptyUIDialogActionType.secondary) { // User confirmed - close the paywall await view.dismiss(); } // If primary - do nothing, user stays } catch (e) { // handle error } ``` ### Cấu hình kiểu trình bày trên iOS \{#configure-ios-presentation-style\} Cấu hình cách flow hoặc paywall được hiển thị trên iOS bằng cách truyền tham số `iosPresentationStyle` vào phương thức `present()`. Tham số này chấp nhận các giá trị `AdaptyUIIOSPresentationStyle.fullScreen` (mặc định) hoặc `AdaptyUIIOSPresentationStyle.pageSheet`. ```dart showLineNumbers try { await view.present(iosPresentationStyle: AdaptyUIIOSPresentationStyle.pageSheet); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ## Nhúng vào cây widget \{#embed-in-widget-hierarchy\} Để nhúng một flow hoặc paywall vào cây widget hiện có của bạn, hãy sử dụng trực tiếp widget `AdaptyUIFlowPlatformView` trong cây widget Flutter của bạn. ```dart showLineNumbers title="Flutter" AdaptyUIFlowPlatformView( flow: flow, // The flow object you fetched onDidAppear: (view) { }, onDidDisappear: (view) { }, onDidPerformAction: (view, action) { }, onDidSelectProduct: (view, productId) { }, onDidStartPurchase: (view, product) { }, onDidFinishPurchase: (view, product, purchaseResult) { }, onDidFailPurchase: (view, product, error) { }, onDidStartRestore: (view) { }, onDidFinishRestore: (view, profile) { }, onDidFailRestore: (view, error) { }, onDidReceiveError: (view, error) { }, onDidFailLoadingProducts: (view, error) { }, onDidFinishWebPaymentNavigation: (view, product, error) { }, ) ``` :::note Để Android platform view hoạt động, hãy đảm bảo `MainActivity` của bạn kế thừa `FlutterFragmentActivity`: ```kotlin showLineNumbers title="Kotlin" class MainActivity : FlutterFragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } } ``` ::: 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 di động để hiển thị cho người dùng. Paywall đó đã bao gồm cả nội dung hiển thị lẫn cách thức hiển thị. :::warning Hướng dẫn này chỉ áp dụng cho **paywall Paywall Builder mới** yêu cầu SDK v3.2.0 trở lên. Quy trình hiển thị paywall khác nhau tùy theo phiên bản Paywall Builder được sử dụng để thiết kế paywall và paywall Remote Config. - Để hiển thị **paywall Remote Config**, xem [Render paywall được thiết kế bằng Remote Config](present-remote-config-paywalls-flutter). ::: Adapty Flutter SDK cung cấp hai cách hiển thị paywall: - **Màn hình độc lập** - **Widget nhúng** ## Hiển thị dưới dạng màn hình độc lập \{#present-as-standalone-screen\} Để hiển thị một paywall dưới dạng màn hình độc lập, hãy sử dụng phương thức `view.present()` trên `view` được tạo bởi phương thức [`createPaywallView`](flutter-get-pb-paywalls#fetch-the-view-configuration-of-paywall-designed-using-paywall-builder). Mỗi `view` chỉ có thể được sử dụng một lần. Nếu bạn cần hiển thị lại paywall, hãy gọi `createPaywallView` thêm một lần nữa để tạo instance `view` mới. :::warning Việc tái sử dụng cùng một `view` mà không tạo lại có thể dẫn đến lỗi `AdaptyUIError.viewAlreadyPresented`. ::: ```dart showLineNumbers title="Flutter" try { await view.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // 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. ::: ### Đóng paywall \{#dismiss-the-paywall\} Khi cần đóng paywall theo chương trình, sử dụng phương thức `dismiss()`: ```dart showLineNumbers title="Flutter" try { await view.dismiss(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ### Hiển thị hộp thoại \{#show-dialog\} Dùng phương thức này thay cho hộp thoại cảnh báo gốc khi paywall view đang được hiển thị trên Android. Trên Android, các alert thông thường sẽ xuất hiện phía sau paywall view, khiến người dùng không thể nhìn thấy. Phương thức này đảm bảo hộp thoại luôn hiển thị đúng vị trí phía trên paywall trên mọi nền tảng. ```dart showLineNumbers title="Flutter" try { final action = await view.showDialog( title: 'Close paywall?', content: 'You will lose access to exclusive offers.', primaryActionTitle: 'Stay', secondaryActionTitle: 'Close', ); if (action == AdaptyUIDialogActionType.secondary) { // User confirmed - close the paywall await view.dismiss(); } // If primary - do nothing, user stays } catch (e) { // handle error } ``` ### Cấu hình kiểu trình bày trên iOS \{#configure-ios-presentation-style\} Cấu hình cách paywall được hiển thị trên iOS bằng cách truyền tham số `iosPresentationStyle` vào phương thức `present()`. Tham số này nhận giá trị `AdaptyUIIOSPresentationStyle.fullScreen` (mặc định) hoặc `AdaptyUIIOSPresentationStyle.pageSheet`. ```dart showLineNumbers try { await view.present(iosPresentationStyle: AdaptyUIIOSPresentationStyle.pageSheet); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ## Nhúng vào cây widget \{#embed-in-widget-hierarchy\} Để nhúng paywall vào cây widget hiện có của bạn, hãy sử dụng widget `AdaptyUIPaywallPlatformView` trực tiếp trong cây widget Flutter của bạn. ```dart showLineNumbers title="Flutter" AdaptyUIPaywallPlatformView( paywall: paywall, // The paywall object you fetched onDidAppear: (view) { }, onDidDisappear: (view) { }, onDidPerformAction: (view, action) { }, onDidSelectProduct: (view, productId) { }, onDidStartPurchase: (view, product) { }, onDidFinishPurchase: (view, product, purchaseResult) { }, onDidFailPurchase: (view, product, error) { }, onDidStartRestore: (view) { }, onDidFinishRestore: (view, profile) { }, onDidFailRestore: (view, error) { }, onDidFailRendering: (view, error) { }, onDidFailLoadingProducts: (view, error) { }, onDidFinishWebPaymentNavigation: (view, product, error) { }, ) ``` :::note Để Android platform view hoạt động, hãy đảm bảo `MainActivity` của bạn extends `FlutterFragmentActivity`: ```kotlin showLineNumbers title="Kotlin" class MainActivity : FlutterFragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } } ``` ::: --- # File: flutter-handle-paywall-actions --- --- title: "Xử lý hành động nút trong Flutter SDK" description: "Xử lý các hành động nút paywall trong Flutter bằng Adapty để tối ưu hóa doanh thu ứng dụng." --- Nếu bạn đang xây dựng flow hoặc paywall bằng Adapty 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 builder](paywall-buttons) và gán cho nút đó một action có sẵn hoặc tạo 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 trình bày cách xử lý các custom action và action có sẵn trong code của bạn. :::warning **Đóng màn hình và mở URL được xử lý tự động** bởi cài đặt mặc định của `flowViewDidPerformAction`, và SDK tự xử lý các giao dịch mua và khôi phục. Tất cả các hành động nút khác, chẳng hạn như đăng nhập hoặc mở một flow khác, cần được xử lý trong mã ứng dụng. Lưu ý rằng việc phản hồi các giao dịch mua và khôi phục *đã hoàn thành* diễn ra trong các callback observer bắt buộc — xem [Xử lý sự kiện flow & paywall](flutter-handling-events). ::: ## Đóng flow và paywall \{#close-flows-and-paywalls\} Để thêm nút đóng flow hoặc paywall, trong builder, hãy thêm một nút và gán cho nó hành động **Close**. Không cần viết code: implementation mặc định của `flowViewDidPerformAction` sẽ tự động đóng view khi nhận được `CloseAction`. :::info Nút **Back** của hệ thống Android không còn tự động đóng view theo mặc định nữa. Nó được chuyển đến `flowViewDidPerformAction` dưới dạng `AndroidSystemBackAction` — bạn cần tự xử lý nếu muốn nút back đóng flow hoặc paywall. ::: Ghi đè `flowViewDidPerformAction` nếu bạn cần hành vi tùy chỉnh — ví dụ: đóng view khi người dùng nhấn nút back của hệ thống Android, như trong v3: ```dart void flowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIAction action) { switch (action) { case const CloseAction(): case const AndroidSystemBackAction(): view.dismiss(); break; case OpenUrlAction(:final url, :final openIn): AdaptyUI().openUrl(url, openIn: openIn); break; default: break; } } ``` :::warning Việc ghi đè `flowViewDidPerformAction` sẽ thay thế hoàn toàn phần cài đặt mặc định — hãy giữ lại các case `CloseAction` và `OpenUrlAction` nếu bạn muốn giữ nguyên hành vi đóng và mở URL mặc định. ::: ## 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 giao dịch mua), hãy thêm phần tử **Link** trong builder và xử lý giống như các nút có hành động **Open URL**. ::: Để thêm nút mở liên kết (ví dụ: **Terms of use** hoặc **Privacy policy**), trong builder, hãy thêm một nút, gán cho nó hành động **Open URL**, rồi nhập URL bạn muốn mở. Không cần viết code: phần triển khai mặc định của `flowViewDidPerformAction` sẽ mở URL theo cách native thông qua `AdaptyUI().openUrl`, tuân theo cài đặt mở trong trình duyệt trong ứng dụng hoặc trình duyệt ngoài từ dashboard. Hành vi mặc định là đủ cho hầu hết các trường hợp. Nếu bạn vẫn muốn tự xử lý việc mở URL, hãy ghi đè handler: ```dart void flowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIAction action) { switch (action) { case const CloseAction(): view.dismiss(); break; case OpenUrlAction(url: final url): // Open the URL in whatever way fits your app break; default: break; } } ``` ## Đăng nhập vào ứng dụng \{#log-into-the-app\} Để thêm nút cho phép người dùng đăng nhập vào ứng dụng: 1. Trong builder, thêm một nút và gán cho nó action **Login**. 2. Trong code ứng dụng, triển khai handler cho action `login` để xác định người dùng của bạn. ```dart void flowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIAction action) { switch (action) { case CustomAction(action: 'login'): // Navigate to your login screen in whatever way fits your app break; default: break; } } ``` ## Xử lý các 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 builder, thêm một nút, gán cho nó hành động **Custom** và đặt cho nó một ID. 2. Trong code của ứng dụng, triển khai một 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 một nút để hiển thị một flow hoặc paywall khác: ```dart void flowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIAction action) { switch (action) { case CustomAction(action: 'openNewPaywall'): // Display another flow or paywall break; default: break; } } ``` 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 một [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 ID hành động tùy chỉnh. 2. Viết code trong ứng dụng của bạn để xử lý từng hành động bạn đã gán. Hướng dẫn này chỉ cách xử lý các hành động tùy chỉnh và có sẵn trong code của bạn. :::warning **Chỉ có giao dịch mua và khôi phục là được xử lý tự động.** Tất cả các hành động nút khác, chẳng hạn như đóng paywall hoặc mở liên kết, đều cần bạn tự triển khai xử lý trong code của ứ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 ứng dụng, triển khai handler cho các hành động `CloseAction` và `AndroidSystemBackAction`. :::info Trong Flutter SDK, các hành động `CloseAction` và `AndroidSystemBackAction` mặc định sẽ kích hoạt việc đó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ở một paywall khác. ::: ```dart void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) { switch (action) { case const CloseAction(): case const AndroidSystemBackAction(): view.dismiss(); break; default: 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 với hành động **Open URL**. ::: Để thêm một nút mở liên kết từ paywall của bạn (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 ứng dụng của bạn, triển khai một handler cho hành động `openUrl` để mở URL nhận được trong trình duyệt. ```dart // You have to install url_launcher plugin in order to handle urls: // https://pub.dev/packages/url_launcher void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) { switch (action) { case OpenUrlAction(url: final url): final Uri uri = Uri.parse(url); launchUrl(uri, mode: LaunchMode.inAppBrowserView); break; default: break; } } ``` ## Đăng nhập vào ứng dụng \{#log-into-the-app\} Để thêm nút cho phép người dùng đăng nhập vào ứng dụng: 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, triển khai một handler cho hành động `login` để xác định người dùng. ```dart void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) { switch (action) { case CustomAction(action: 'login'): // Navigate to your login screen in whatever way fits your app break; default: break; } } ``` ## Xử lý custom action \{#handle-custom-actions\} Để thêm nút xử lý bất kỳ hành động nào khác: 1. Trong Paywall Builder, thêm một nút, gán cho nó hành động **Custom**, và đặt cho nó một ID. 2. Trong code của ứng dụng, hãy triển khai handler cho action ID mà bạn đã tạo. Ví dụ: nếu bạn có một bộ ưu đã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ị paywall khác: ```dart void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) { switch (action) { case CustomAction(action: 'openNewPaywall'): // Display another paywall break; default: break; } } ``` --- # File: flutter-handling-events --- --- title: "Flutter - Xử lý sự kiện flow & paywall" description: "Khám phá cách xử lý các sự kiện liên quan đến gói đăng ký trong Flutter bằng Adapty để theo dõi tương tác người dùng hiệu quả." --- :::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, lựa chọn sản phẩm và hiển thị. Việc đóng view và mở liên kết được xử lý bởi implementation mặc định của `flowViewDidPerformAction` — xem [hướng dẫn xử lý hành động button](flutter-handle-paywall-actions) để ghi đè chúng hoặc xử lý các hành động button tùy chỉnh. ::: Flows và paywalls được cấu hình bằng 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. Những sự kiện này bao gồm các lần 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 diễn ra trên flow hoặc paywall. Tìm hiểu cách phản hồi 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 flow hoặc paywall trong ứng dụng di động của bạn, hãy triển khai các phương thức `AdaptyUIFlowsEventsObserver` và thiết lập observer trước khi hiển thị bất kỳ màn hình nào: ```dart showLineNumbers title="Flutter" AdaptyUI().setFlowsEventsObserver(this); ``` Ba phương thức observer là **bắt buộc** — class của bạn sẽ không biên dịch được nếu thiếu chúng: `flowViewDidFinishPurchase`, `flowViewDidFinishRestore`, và `flowViewDidReceiveError`. Tất cả các phương thức còn lại là tùy chọn. Để hủy kết nối một observer đã được thiết lập trước đó, truyền `null` vào `setFlowsEventsObserver`. :::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. ::: Các ví dụ sự kiện dưới đây hiển thị các thuộc tính có sẵn trên từng đối tượng, với các giá trị minh họa trong phần chú thích. ### Sự kiện do người dùng tạo ra \{#user-generated-events\} #### View appeared \{#view-appeared\} Phương thức này được gọi khi flow hoặc màn hình paywall xuất hiện trên màn hình. :::note Trên iOS, phương thức này cũng được gọi khi người dùng nhấn vào [nút web paywall](web-paywall#step-2a-add-a-web-purchase-button) bên trong một paywall, và web paywall mở ra trong trình duyệt trong ứng dụng. ::: ```dart showLineNumbers title="Flutter" void flowViewDidAppear(AdaptyUIFlowView view) { } ``` #### View disappeared \{#view-disappeared\} Phương thức này được gọi khi flow hoặc màn hình paywall bị đóng khỏi màn hình. :::note Trên iOS, cũng được gọi khi một [web paywall](web-paywall#step-2a-add-a-web-purchase-button) mở từ paywall trong trình duyệt trong ứng dụng biến mất khỏi màn hình. ::: ```dart showLineNumbers title="Flutter" void flowViewDidDisappear(AdaptyUIFlowView view) { } ``` #### Chọn sản phẩm \{#product-selection\} Nếu một sản phẩm được chọn để mua (bởi người dùng hoặc hệ thống), phương thức này sẽ được gọi: ```dart showLineNumbers title="Flutter" void flowViewDidSelectProduct(AdaptyUIFlowView view, String productId) { } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```dart void flowViewDidSelectProduct(AdaptyUIFlowView view, String productId) { // productId is a String: productId; // 'premium_monthly' } ```
#### Đã 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: ```dart showLineNumbers title="Flutter" void flowViewDidStartPurchase(AdaptyUIFlowView view, AdaptyPaywallProduct product) { } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```dart void flowViewDidStartPurchase(AdaptyUIFlowView view, AdaptyPaywallProduct product) { // product — AdaptyPaywallProduct: product.vendorProductId; // 'premium_monthly' product.localizedTitle; // 'Premium Monthly' product.localizedDescription; // 'Premium subscription for 1 month' product.price.amount; // 9.99 (double) product.price.currencyCode; // 'USD' product.price.localizedString; // '$9.99' } ```
#### Hoàn tất giao dịch mua \{#finished-purchase\} Phương thức này là **bắt buộc**. Nó được gọi khi một giao dịch mua thành công, người dùng hủy giao dịch, hoặc giao dịch có vẻ đang chờ xử lý: ```dart showLineNumbers title="Flutter" void flowViewDidFinishPurchase(AdaptyUIFlowView view, AdaptyPaywallProduct product, AdaptyPurchaseResult purchaseResult) { switch (purchaseResult) { case AdaptyPurchaseResultSuccess(profile: final profile): // successful purchase break; case AdaptyPurchaseResultPending(): // purchase is pending break; case AdaptyPurchaseResultUserCancelled(): // user cancelled the purchase break; default: break; } } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```dart void flowViewDidFinishPurchase(AdaptyUIFlowView view, AdaptyPaywallProduct product, AdaptyPurchaseResult purchaseResult) { // product — AdaptyPaywallProduct: product.vendorProductId; // 'premium_monthly' switch (purchaseResult) { case AdaptyPurchaseResultSuccess(profile: final profile): // profile — AdaptyProfile: profile.accessLevels['premium']?.isActive; // true profile.accessLevels['premium']?.expiresAt; // DateTime(2027, 2, 15, 10, 30) break; case AdaptyPurchaseResultPending(): // no additional data break; case AdaptyPurchaseResultUserCancelled(): // no additional data break; } } ```
:::info Khác với v3, phương thức này không có hành vi mặc định — view sẽ không tự động bị đóng sau khi mua thành công. Bạn tự quyết định bước tiếp theo: tiếp tục flow hoặc gọi `view.dismiss()`. Tham khảo [Xử lý hành động nút](flutter-handle-paywall-actions) để biết thêm chi tiết về cách đóng màn hình. ::: #### Hoàn tất điều hướng thanh toán web \{#finished-web-payment-navigation\} Phương thức này được gọi sau khi có một lần thử mở [web paywall](web-paywall) cho một sản phẩm cụ thể. Điều này bao gồm cả các lần điều hướng thành công lẫn thất bại: ```dart showLineNumbers title="Flutter" void flowViewDidFinishWebPaymentNavigation(AdaptyUIFlowView view, AdaptyPaywallProduct? product, AdaptyError? error) { } ``` **Tham số:** | Tham số | Mô tả | |:------------|:---------------------------------------------------------------------------------------------------| | **product** | Một `AdaptyPaywallProduct` mà web paywall đã được mở. Có thể là `null`. | | **error** | Một đối tượng `AdaptyError` nếu điều hướng web paywall thất bại; `null` nếu điều hướng thành công. | #### Giao dịch thất bại \{#failed-purchase\} Phương thức này được gọi khi một giao dịch mua thất bại (ví dụ: do sự cố thanh toán hoặc lỗi mạng). Nó **không** kích hoạt khi người dùng tự hủy hoặc khi giao dịch đang chờ xử lý—những trường hợp đó được xử lý bởi `flowViewDidFinishPurchase`: ```dart showLineNumbers title="Flutter" void flowViewDidFailPurchase(AdaptyUIFlowView view, AdaptyPaywallProduct product, AdaptyError error) { } ``` #### Bắt đầu khôi phục \{#started-restore\} Nếu người dùng bắt đầu quá trình khôi phục, phương thức này sẽ được gọi: ```dart showLineNumbers title="Flutter" void flowViewDidStartRestore(AdaptyUIFlowView view) { } ``` #### Khôi phục thành công \{#successful-restore\} Phương thức này là **bắt buộc**. Nếu việc khôi phục mua hàng thành công, nó sẽ được gọi: ```dart showLineNumbers title="Flutter" void flowViewDidFinishRestore(AdaptyUIFlowView view, AdaptyProfile profile) { } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```dart void flowViewDidFinishRestore(AdaptyUIFlowView view, AdaptyProfile profile) { // profile — AdaptyProfile: profile.accessLevels['premium']?.isActive; // true profile.accessLevels['premium']?.expiresAt; // DateTime(2027, 2, 15, 10, 30) profile.subscriptions['premium_monthly']?.isActive; // true profile.subscriptions['premium_monthly']?.expiresAt; // DateTime(2027, 2, 15, 10, 30) } ```
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ý](flutter-listen-subscription-changes) để tìm hiểu cách kiểm tra và chủ đề [Phản hồi các hành động nút](flutter-handle-paywall-actions) để tìm hiểu cách đóng màn hình. #### Khôi phục thất bại \{#failed-restore\} Nếu việc khôi phục giao dịch mua thất bại, phương thức này sẽ được gọi: ```dart showLineNumbers title="Flutter" void flowViewDidFailRestore(AdaptyUIFlowView view, AdaptyError error) { } ``` ### 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ừ máy chủ. 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: ```dart showLineNumbers title="Flutter" void flowViewDidFailLoadingProducts(AdaptyUIFlowView view, AdaptyError error) { } ``` #### Lỗi view \{#view-errors\} Phương thức này là **bắt buộc**. Nó thay thế phương thức `paywallViewDidFailRendering` của v3: các lỗi xảy ra trong quá trình render giao diện, cũng như các lỗi view khác, sẽ được báo cáo thông qua lần gọi này. Sau khi bạn implement nó, việc đóng view là tùy bạn quyết định — chúng tôi khuyến nghị đóng view khi gặp các lỗi như vậy, đây cũng chính là hành vi mặc định tích hợp sẵn của SDK khi không có observer nào được thiết lập: ```dart showLineNumbers title="Flutter" void flowViewDidReceiveError(AdaptyUIFlowView view, AdaptyError error) { // log the error and dismiss the broken view view.dismiss(); } ``` Trong trường hợp bình thường, lỗi hiển thị 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. ### Sự kiện phân tích \{#analytics-events\} Phương thức tùy chọn `flowViewDidReceiveAnalyticEvent` được dành riêng cho các sự kiện phân tích tùy chỉnh từ một flow. Hiện tại, các flow chưa phát các sự kiện này đến code của bạn, vì vậy bạn chưa cần triển khai nó. ### Xử lý giao dịch mua hàng trong Observer mode \{#handle-purchases-in-observer-mode\} Nếu bạn đã kích hoạt SDK ở chế độ [Observer mode](implement-observer-mode-flutter) và hiển thị một flow hoặc paywall do Adapty render, SDK sẽ không thực hiện giao dịch mua hàng thay bạn. Khi người dùng nhấn nút mua hàng hoặc khôi phục, SDK sẽ gọi `AdaptyUIObserverModeResolver` của bạn. Xem [Hiển thị flow trong Observer mode](flutter-present-flows-in-observer-mode) để biết cách thiết lập đầy đủ. ### Xử lý các yêu cầu hệ thống \{#handle-system-requests\} `AdaptyUISystemRequestsHandler` (đăng ký qua `AdaptyUI().setSystemRequestsHandler(...)`) được dành riêng cho các yêu cầu hệ thống từ một flow: các lời nhắc cấp quyền của hệ điều hành (như thông báo đẩy hoặc quyền truy cập camera) và yêu cầu đánh giá trên App Store. Hiện tại các flow chưa kích hoạt những yêu cầu này, nên bạn chưa cần đăng ký handler. Nếu bạn có đăng ký một cái, hãy lưu ý rằng `handlePermission` là phương thức bắt buộc của class — hãy yêu cầu quyền bằng code của riêng bạn, sau đó trả về `AdaptyUIPermissionResult.granted()` hoặc `AdaptyUIPermissionResult.denied()`; còn `handleAppReviewRequest` là tùy chọ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 (đóng paywall, mở liên kết, v.v.). Xem [hướng dẫn xử lý hành động nút](flutter-handle-paywall-actions) để biết thêm chi tiết. ::: Các 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 các lần 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 xem cách phản hồi các sự kiện này bên dưới. :::warning 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. ::: Để kiểm soát hoặc theo dõi các sự kiện xảy ra trên màn hình paywall trong ứng dụng di động của bạn, hãy implement các phương thức `AdaptyUIPaywallsEventsObserver` và đặt observer trước khi hiển thị bất kỳ màn hình nào: ```dart showLineNumbers title="Flutter" AdaptyUI().setPaywallsEventsObserver(this); ``` :::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. ::: Các ví dụ về sự kiện bên dưới hiển thị các thuộc tính có sẵn trên mỗi đối tượng, cùng với các giá trị minh họa trong phần chú thích. ### Sự kiện do người dùng tạo ra \{#user-generated-events\} #### Paywall xuất hiện \{#paywall-appeared\} Phương thức này được gọi khi màn hình paywall hiển thị trên màn hình. :::note Trên iOS, 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ở ra trong trình duyệt trong ứng dụng. ::: ```dart showLineNumbers title="Flutter" void paywallViewDidAppear(AdaptyUIPaywallView view) { } ``` #### Paywall biến mất \{#paywall-disappeared\} Phương thức này được gọi khi màn hình paywall bị đóng khỏi màn hình. :::note Trên iOS, cũng được gọi khi một [web paywall](web-paywall#step-2a-add-a-web-purchase-button) mở từ một paywall trong trình duyệt trong ứng dụng biến mất khỏi màn hình. ::: ```dart showLineNumbers title="Flutter" void paywallViewDidDisappear(AdaptyUIPaywallView view) { } ``` #### Chọn sản phẩm \{#product-selection\} Nếu một sản phẩm được chọn để mua (bởi người dùng hoặc bởi hệ thống), phương thức này sẽ được gọi: ```dart showLineNumbers title="Flutter" void paywallViewDidSelectProduct(AdaptyUIPaywallView view, String productId) { } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```dart void paywallViewDidSelectProduct(AdaptyUIPaywallView view, String productId) { // productId is a String: productId; // 'premium_monthly' } ```
#### Bắt đầu mua hàng \{#started-purchase\} Khi người dùng bắt đầu quá trình mua hàng, phương thức này sẽ được gọi: ```dart showLineNumbers title="Flutter" void paywallViewDidStartPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product) { } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```dart void paywallViewDidStartPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product) { // product — AdaptyPaywallProduct: product.vendorProductId; // 'premium_monthly' product.localizedTitle; // 'Premium Monthly' product.localizedDescription; // 'Premium subscription for 1 month' product.price.amount; // 9.99 (double) product.price.currencyCode; // 'USD' product.price.localizedString; // '$9.99' } ```
#### Hoàn tất thanh toán \{#finished-purchase\} Phương thức này được gọi khi giao dịch mua thành công, người dùng hủy giao dịch, hoặc giao dịch đang ở trạng thái chờ xử lý: ```dart showLineNumbers title="Flutter" void paywallViewDidFinishPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyPurchaseResult purchaseResult) { switch (purchaseResult) { case AdaptyPurchaseResultSuccess(profile: final profile): // successful purchase break; case AdaptyPurchaseResultPending(): // purchase is pending break; case AdaptyPurchaseResultUserCancelled(): // user cancelled the purchase break; default: break; } } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```dart void paywallViewDidFinishPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyPurchaseResult purchaseResult) { // product — AdaptyPaywallProduct: product.vendorProductId; // 'premium_monthly' switch (purchaseResult) { case AdaptyPurchaseResultSuccess(profile: final profile): // profile — AdaptyProfile: profile.accessLevels['premium']?.isActive; // true profile.accessLevels['premium']?.expiresAt; // DateTime(2027, 2, 15, 10, 30) break; case AdaptyPurchaseResultPending(): // no additional data break; case AdaptyPurchaseResultUserCancelled(): // no additional data break; } } ```
Chúng tôi khuyến nghị đóng màn hình trong trường hợp đó. Tham khảo [Phản hồi các hành động nút](flutter-handle-paywall-actions) để biết chi tiết về cách đóng màn hình paywall. #### Hoàn tất điều hướng thanh toán web \{#finished-web-payment-navigation\} Phương thức này được gọi sau khi có lần thử mở [web paywall](web-paywall) cho một sản phẩm cụ thể. Điều này bao gồm cả các lần điều hướng thành công và thất bại: ```dart showLineNumbers title="Flutter" void paywallViewDidFinishWebPaymentNavigation(AdaptyUIPaywallView view, AdaptyPaywallProduct? product, AdaptyError? error) { } ``` **Tham số:** | Tham số | Mô tả | |:------------|:---------------------------------------------------------------------------------------------------| | **product** | Một `AdaptyPaywallProduct` mà web paywall được mở cho. Có thể là `null`. | | **error** | Một đối tượng `AdaptyError` nếu điều hướng web paywall thất bại; `null` nếu điều hướng thành công. |
Ví dụ về sự kiện (Nhấn để mở rộng) ```dart void paywallViewDidFinishWebPaymentNavigation(AdaptyUIPaywallView view, AdaptyPaywallProduct? product, AdaptyError? error) { // product — AdaptyPaywallProduct?: product?.vendorProductId; // 'premium_monthly' if (error == null) { // navigation succeeded } else { // error — AdaptyError: error.code; // AdaptyErrorCode.networkFailed (2005) error.message; // 'Network request failed' error.detail; // platform-specific underlying error, or null } } ```
#### Mua hàng thất bại \{#failed-purchase\} Phương thức này được gọi khi một giao dịch mua thất bại (ví dụ: do lỗi thanh toán hoặc lỗi mạng). Nó **không** kích hoạt khi người dùng chủ động huỷ hoặc giao dịch đang chờ xử lý — những trường hợp đó được xử lý bởi `paywallViewDidFinishPurchase`: ```dart showLineNumbers title="Flutter" void paywallViewDidFailPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyError error) { } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```dart void paywallViewDidFailPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyError error) { // product — AdaptyPaywallProduct: product.vendorProductId; // 'premium_monthly' // error — AdaptyError: error.code; // AdaptyErrorCode.productPurchaseFailed (1006) error.message; // 'Product purchase failed.' error.detail; // platform-specific underlying error, or null } ```
#### Bắt đầu khôi phục \{#started-restore\} Nếu người dùng khởi động quá trình khôi phục, phương thức này sẽ được gọi: ```dart showLineNumbers title="Flutter" void paywallViewDidStartRestore(AdaptyUIPaywallView view) { } ``` #### 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: ```dart showLineNumbers title="Flutter" void paywallViewDidFinishRestore(AdaptyUIPaywallView view, AdaptyProfile profile) { } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```dart void paywallViewDidFinishRestore(AdaptyUIPaywallView view, AdaptyProfile profile) { // profile — AdaptyProfile: profile.accessLevels['premium']?.isActive; // true profile.accessLevels['premium']?.expiresAt; // DateTime(2027, 2, 15, 10, 30) profile.subscriptions['premium_monthly']?.isActive; // true profile.subscriptions['premium_monthly']?.expiresAt; // DateTime(2027, 2, 15, 10, 30) } ```
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ý](flutter-listen-subscription-changes) để biết cách kiểm tra và chủ đề [Xử lý hành động nút](flutter-handle-paywall-actions) để biết cách đóng màn hình paywall. #### 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: ```dart showLineNumbers title="Flutter" void paywallViewDidFailRestore(AdaptyUIPaywallView view, AdaptyError error) { } ```
Ví dụ sự kiện (Nhấp để mở rộng) ```dart void paywallViewDidFailRestore(AdaptyUIPaywallView view, AdaptyError error) { // error — AdaptyError: error.code; // AdaptyErrorCode.receiveRestoredTransactionsFailed (1011) error.message; // 'Error occurred in the process of restoring purchases.' error.detail; // platform-specific underlying error, or null } ```
### 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: ```dart showLineNumbers title="Flutter" void paywallViewDidFailLoadingProducts(AdaptyUIPaywallView view, AdaptyError error) { } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```dart void paywallViewDidFailLoadingProducts(AdaptyUIPaywallView view, AdaptyError error) { // error — AdaptyError: error.code; // AdaptyErrorCode.productRequestFailed (1002) error.message; // 'Unable to fetch available In-App Purchase products at the moment.' error.detail; // platform-specific underlying error, or null } ```
#### Lỗi khi hiển thị \{#rendering-errors\} Nếu xảy ra lỗi trong quá trình render giao diện, lỗi đó sẽ được báo cáo thông qua phương thức này. Theo mặc định (từ v3.15.2), paywall sẽ tự động bị đóng khi gặp lỗi render, nhưng bạn có thể ghi đè hành vi này nếu cần. ```dart showLineNumbers title="Flutter" void paywallViewDidFailRendering(AdaptyUIPaywallView view, AdaptyError error) { // Default behavior: view.dismiss() // Override with custom logic if needed, for example: // - Log the error // - Show an error message to the user } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```dart void paywallViewDidFailRendering(AdaptyUIPaywallView view, AdaptyError error) { // error — AdaptyError: error.code; // AdaptyErrorCode.jsException (4105) error.message; // 'An exception was thrown from JS during AdaptyUI flow execution.' error.detail; // platform-specific underlying error, or null // Default behavior: view.dismiss() } ```
Trong trường hợp 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: flutter-use-fallback-paywalls --- --- title: "Flutter - Sử dụng paywall dự phòng" 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" --- :::warning Paywall dự phòng được hỗ trợ từ Flutter SDK v2.11 trở lên. ::: Để 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 các file cấu hình dự phòng vào thư mục `assets` của ứng dụng tại thư mục gốc của dự án. 2. Gọi phương thức `.setFallback` **trước khi** bạn tải paywall hoặc onboarding đích. ```dart showLineNumbers title="Flutter" final assetId = Platform.isIOS ? 'assets/ios_fallback.json' : 'assets/android_fallback.json'; try { await Adapty().setFallback(assetId); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Tham số: | Tham số | Mô tả | | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **assetId** | Đường dẫn đến file cấu hình dự phò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: flutter-localizations-and-locale-codes --- --- title: "Sử dụng localizations và locale codes trong Flutter SDK" description: "Quản lý localizations và locale codes của ứng dụng để tiếp cận người dùng toàn cầu." --- ## Tại sao điều này quan trọng \{#why-this-is-important\} Mã ngôn ngữ (locale code) được sử dụng khi Adapty chọn bản dịch cho một flow, và khi bạn đọc Remote Config cho một paywall tùy chỉnh. Mã ngôn ngữ khá phức tạp và có thể khác nhau giữa các nền tảng, vì vậy Adapty sử dụng một tiêu chuẩn nội bộ thống nhất trên mọi nền tảng được hỗ trợ. Hiểu được tiêu chuẩn đó sẽ giúp bạn dự đoán bản dịch nào người dùng sẽ nhận được. ## Tiêu chuẩn mã ngôn ngữ tại Adapty \{#locale-code-standard-at-adapty\} Đối với mã ngôn ngữ, Adapty sử dụng một phiên bản điều chỉnh nhỏ của [tiêu chuẩn BCP 47](https://en.wikipedia.org/wiki/IETF_language_tag): mỗi mã bao gồm các subtag viết thường, phân cách bằng dấu gạch ngang. Một số 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ể). ## Khớp mã ngôn ngữ \{#locale-code-matching\} Khi Adapty tìm kiếm bản địa hóa phù hợp với ngôn ngữ của người dùng, quá trình diễn ra như sau: 1. Chuỗi ngôn ngữ được chuyển thành chữ thường và tất cả dấu gạch dưới (`_`) được thay thế bằng dấu gạch ngang (`-`) 2. Adapty tìm kiếm bản địa hóa có mã ngôn ngữ khớp hoàn toàn 3. Nếu không tìm thấy, Adapty lấy chuỗi con trước dấu gạch ngang đầu tiên (`pt` từ `pt-br`) và tìm kiếm bản địa hóa phù hợp 4. Nếu vẫn không tìm thấy, Adapty trả về bản địa hóa mặc định `en` Nhờ cách này, `'pt_BR'`, `pt-BR` và `pt-br` đều được phân giải về cùng một bản địa hóa. ## Triển khai localization \{#implementing-localizations\} Trong SDK v4, bạn không cần truyền mã locale khi lấy một flow. - **Paywall dùng Flow Builder và Paywall Builder**: Adapty tự động xác định localization dựa trên thiết bị và các localization bạn đã cấu hình trong builder. Render flow bằng `createFlowView` — không cần mã locale. - **Paywall tùy chỉnh (remote config)**: `getFlow` trả về tất cả các localization đã cấu hình trong `flow.remoteConfigs`. Mỗi mục có mã `locale` và nội dung config (chuỗi `data`, hoặc `dictionary` đã được parse). Hãy chọn mục phù hợp với người dùng, kèm theo logic fallback của riêng bạn: ```dart showLineNumbers final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID'); final config = flow.remoteConfigs.firstWhereOrNull((c) => c.locale == 'en') ?? flow.remoteConfig; // the first remote config, if present // read your values from config?.dictionary ``` Các quy tắc khớp mã locale ở trên mô tả cách Adapty chuẩn hóa các mã `locale` được lưu trữ trên mỗi Remote Config. ## Tại sao điều này quan trọng \{#why-this-is-important\} Có một vài tình huống mà mã ngôn ngữ (locale code) trở nên quan trọng — ví dụ, khi bạn cần lấy đúng paywall phù hợp với ngôn ngữ hiện tại của ứng dụng. Vì mã ngôn ngữ khá phức tạp và có thể khác nhau tùy từng nền tảng, chúng tôi sử dụng một tiêu chuẩn nội bộ thống nhất 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 bản ngôn ngữ mong muốn — và điều gì xảy ra tiếp theo — để luôn nhận được kết quả như kỳ vọng. ## Tiêu chuẩn mã ngôn ngữ tại Adapty \{#locale-code-standard-at-adapty\} Đối với mã ngôn ngữ, Adapty sử dụng chuẩn [BCP 47](https://en.wikipedia.org/wiki/IETF_language_tag) được chỉnh sửa nhẹ: mỗi mã bao gồm các subtag viết thường, ngăn cách nhau bằng dấu gạch ngang. Một số 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 sánh mã ngôn ngữ \{#locale-code-matching\} Khi Adapty nhận được lệnh gọi từ SDK phía client kèm theo mã ngôn ngữ và bắt đầu tìm kiếm bản bản địa hóa tương ứng của paywall, quá trình diễn ra như sau: 1. Chuỗi ngôn ngữ đầu vào được chuyển thành chữ thường và tất cả dấu gạch dưới (`_`) được thay thế bằng dấu gạch ngang (`-`) 2. Chúng tôi tìm kiếm bản bản địa hóa có mã ngôn ngữ khớp hoàn toàn 3. Nếu không tìm thấy kết quả khớp, chúng tôi lấy chuỗi con trước dấu gạch ngang đầu tiên (`pt` trong `pt-br`) và tìm kiếm bản bản địa hóa tương ứng 4. Nếu vẫn không tìm thấy kết quả khớp, chúng tôi trả về bản bản địa hóa mặc định `en` Bằng cách nà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 sẽ nhận được cùng một kết quả. ## Triển khai bản địa hóa: cách được khuyến nghị \{#implementing-localizations-recommended-way\} Nếu bạn đang tìm hiểu về bản địa hóa, có thể bạn đã làm việc với các file chuỗi đã được bản địa hóa trong dự án của mình. Trong trường hợp đó, chúng tôi khuyến nghị bạn thêm một cặp key-value với mã locale Adapty tương ứng vào mỗi file cho từng bản địa hóa. Sau đó, trích xuất giá trị của key đó khi gọi SDK, như ví dụ dưới đây: ```dart showLineNumbers // 1. Modify your app_en.arb, app_es.arb, app_pt_br.arb files /* app_en.arb */ "adapty_paywalls_locale": "en", /* app_es.arb */ "adapty_paywalls_locale": "es", /* app_pt_br.arb */ "adapty_paywalls_locale": "pt-br", // 2. Extract and use the locale code final locale = AppLocalizations.of(context)!.adapty_paywalls_locale; // pass locale code to AdaptyUI.getViewConfiguration or Adapty.getPaywall method ``` Bằng cách này, bạn có thể đảm bảo kiểm soát hoàn toàn ngôn ngữ nào sẽ được tải về cho từng người dùng trong ứng dụng của bạn. ## Triển khai bản địa hóa theo 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õ ràng mã ngôn ngữ cho từng bản địa hóa. Điều đó có nghĩa là trích xuất mã ngôn ngữ từ các đối tượng khác mà nền tảng của bạn cung cấp, như sau: ```dart showLineNumbers final locale = Localizations.localeOf(context).languageCode; // 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 tiếp cận này vì một số lý do: 1. Trên iOS, ngôn ngữ ưu tiên và locale hiện tại không giống nhau. Nếu muốn bản địa hóa được chọn đúng, bạn cần dựa vào logic của Apple — hoạt động tự động nếu bạn dùng cách tiếp cận được khuyến nghị với các file string đã bản địa hóa — hoặc tự tái tạo lại logic đó. 2. Rất khó dự đoán chính xác những gì server của Adapty sẽ nhận được. Ví dụ: trên iOS, có thể lấy được một locale như `ar_OM@numbers='latn'` từ thiết bị và gửi lên server. Khi đó, thay vì nhận được bản địa hóa `ar-om` như mong muốn, bạn sẽ nhận được `ar` — điều này có thể nằm ngoài kỳ vọng. Should you decide to use this approach anyway — make sure you've covered all the relevant use cases. --- # File: flutter-web-paywall --- --- title: "Triển khai web paywall trong Flutter 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 trong dashboard](web-paywall) và cài đặt Adapty SDK phiên bản 3.6.1 trở lên. ::: 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ể đang hiển thị cho người dùng với trang web 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ẽ kích hoạt trong ứng dụng gần như ngay lập tức. ```dart showLineNumbers title="Flutter" try { await Adapty().openWebPaywall(product: ); // The web paywall will be opened } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle other errors } ``` :::note Có hai phiên bản của phương thức `openWebPaywall`: 1. `openWebPaywall(product)` tạo URL theo paywall và 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 khi các sản phẩm trong Adapty paywall khác với các sản phẩm trong web paywall. Trong SDK v4, tham số `paywall` nhận vào một `AdaptyFlowPaywall` — một biến thể paywall của flow đã được tải về. Hãy kiểm tra `flow.paywalls` không rỗng trước khi truy cập theo chỉ số, ví dụ `flow.paywalls[0]`. ::: #### 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 cách 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 mua hàng thay thế | | AdaptyError.failedDecodingWebPaywallUrl | Không thể mã hóa các tham số trong URL đúng cách | Xác minh các tham số URL hợp lệ và được định dạng đú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. ::: Theo mặc định, web paywall sẽ mở trong trình duyệt 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 trong ứng dụng. Cách này hiển thị trang mua hàng ngay bên trong ứng dụng, giú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, hãy đặt tham số `in` thành `.inAppBrowser`: ```dart showLineNumbers try { await Adapty().openWebPaywall( product: , openIn: AdaptyWebPresentation.inAppBrowser, ); // The web paywall will be opened in the in-app browser } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle other errors } ``` --- # File: flutter-troubleshoot-paywall-builder --- --- title: "Khắc phục sự cố Paywall Builder trong Flutter SDK" description: "Khắc phục sự cố Paywall Builder trong Flutter 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 với Flutter SDK. ## Lấy cấu hình paywall thất bại \{#getting-a-paywall-configuration-fails\} **Vấn đề**: Phương thức `createPaywallView` không lấy được 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\} **Vấn đề**: Số lượt xem paywall hiển thị gấp đôi con số mong đợi. **Nguyên nhân**: Bạn có thể đang gọi `logShowFlow` (Flutter SDK v4+) / `logShowPaywall` trong code, khiến số lượt xem bị nhân đôi 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 các công cụ này, analytics được theo dõi tự động, vì vậy bạn không cần dùng phương thức này. **Giải pháp**: Đảm bảo bạn không gọi `logShowFlow` (Flutter SDK v4+) / `logShowPaywall` trong code nếu đang sử dụng Paywall Builder hoặc Flow Builder. ## Các vấn đề khác \{#other-issues\} **Vấn đề**: Bạn đang gặp phải các sự cố liên quan đến Paywall Builder 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](flutter-sdk-migration-guides) nếu cần. Nhiều vấn đề đã được giải quyết trong các phiên bản SDK mới hơn. --- # File: flutter-present-flows-in-observer-mode --- --- title: "Hiển thị flow trong Observer mode trong Flutter SDK" description: "Hiển thị flow và paywall được tạo bằng Paywall Builder trong Observer mode trong ứng dụng Flutter của bạn trong khi xử lý các giao dịch mua bằng code của riêng bạn." --- Nếu bạn đã tùy chỉnh flow hoặc paywall bằng builder, bạn không cần lo lắng về việc render nó trong code ứng dụng di động để hiển thị cho người dùng. Flow hoặc paywall như vậy đã bao gồm cả nội dung hiển thị lẫn cách 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 ở Observer mode, hãy tham khảo chủ đề [Hiển thị flows & paywalls](flutter-present-paywalls). ::: :::info Tính năng này yêu cầu Adapty Flutter SDK 4.0 trở lên — trước đây chỉ có trong các SDK gốc iOS và Android. Xem [hướng dẫn migration](migration-to-flutter-sdk-v4) để nâng cấp. :::
Trước khi bắt đầu hiển thị flow (Nhấp để mở rộng) 1. Thiết lập tích hợp ban đầu của Adapty [với App Store](initial_ios) và [với Google Play](initial-android). 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 Flutter SDK](sdk-installation-flutter#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 vào chúng. 5. [Tạo placement và gán flow hoặc paywall vào chúng](create-placement). 6. [Lấy flow và cấu hình của chúng](flutter-get-pb-paywalls) trong code ứng dụng di động của bạn.
Trong Observer mode, SDK không thực hiện giao dịch mua hàng thay cho bạn. Khi người dùng nhấn nút mua hàng hoặc khôi phục trong một flow hoặc paywall do Adapty hiển thị, SDK sẽ gọi `AdaptyUIObserverModeResolver` của bạn — hãy thực hiện việc mua hàng hoặc khôi phục bằng code của chính bạn tại đó. 1. Implement `AdaptyUIObserverModeResolver`: ```dart showLineNumbers title="Flutter" class MyObserverModeResolver extends AdaptyUIObserverModeResolver { @override void observerModeDidInitiatePurchase( AdaptyUIFlowView view, AdaptyPaywallProduct product, void Function() onStartPurchase, void Function() onFinishPurchase, ) { onStartPurchase(); // the view shows its loading indicator // make the purchase with your own code, then: onFinishPurchase(); // the view hides the loading indicator } @override void observerModeDidInitiateRestore( AdaptyUIFlowView view, void Function() onStartRestore, void Function() onFinishRestore, ) { onStartRestore(); // restore purchases with your own code, then: onFinishRestore(); } } ``` `observerModeDidInitiatePurchase` thông báo cho bạn biết rằng người dùng đã khởi tạo một giao dịch mua, và `observerModeDidInitiateRestore` — rằng người dùng đã khởi tạo thao tác khôi phục. Hãy kích hoạt flow mua hàng hoặc khôi phục tùy chỉnh của bạn để phản hồi. Ngoài ra, hãy nhớ gọi các callback sau để thông báo cho AdaptyUI về quá trình mua hàng hoặc khôi phục. Điều này cần thiết để đảm bảo flow hoạt động đúng, chẳng hạn như hiển thị loader, v.v.: | Callback | Mô tả | | :----------------- | :------------------------------------------------------------------------------------------- | | onStartPurchase() | Callback này cần được gọi để thông báo cho AdaptyUI rằng quá trình mua hàng đã bắt đầu. | | onFinishPurchase() | Callback này cần được gọi để thông báo cho AdaptyUI rằng quá trình mua hàng đã hoàn thành. | | onStartRestore() | Callback này cần được gọi để thông báo cho AdaptyUI rằng quá trình khôi phục đã bắt đầu. | | onFinishRestore() | Callback này cần được gọi để thông báo cho AdaptyUI rằng quá trình khôi phục đã hoàn thành. | 2. Đăng ký resolver trước khi hiển thị bất kỳ màn hình nào: ```dart showLineNumbers title="Flutter" AdaptyUI().setObserverModeResolver(MyObserverModeResolver()); ``` 3. Tạo và hiển thị flow view như bình thường: [lấy flow và tạo view của nó](flutter-get-pb-paywalls), sau đó [hiển thị nó](flutter-present-paywalls). Không cần thêm tham số nào — một khi resolver đã được đăng ký, mọi flow hoặc paywall do Adapty render đều sẽ xử lý việc mua và khôi phục thông qua nó. :::warning Đừng quên [báo cáo giao dịch và liên kết nó với paywall](report-transactions-observer-mode-flutter). Nếu không, Adapty sẽ không nhận ra giao dịch và sẽ không xác định được paywall nguồn của lần mua. ::: --- # File: flutter-quickstart-manual --- --- title: "Bật tính năng mua hàng trong paywall tùy chỉnh với Flutter SDK" description: "Tích hợp Adapty SDK vào các paywall Flutter tùy chỉnh của bạn để bật tính năng 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 SDK của Adapty tự động lấy sản phẩm, xử lý các giao dịch mới và khôi phục các giao dịch trước đó. Hướng dẫn này sử dụng API của Adapty Flutter SDK v4 — nếu bạn đang dùng v3, hãy xem [hướng dẫn migration](migration-to-flutter-sdk-v4) để biết tên các phương thức tương ứng. :::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 tính năng mua hàng, hãy sử dụng [Adapty Paywall Builder](flutter-quickstart-paywalls). Với Paywall Builder, bạn tạo paywall bằng trình chỉnh sửa trực quan không cần code, Adapty xử lý toàn bộ logic mua hàng tự độ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 tính năng in-app purchase, bạn cần nắm vững ba khái niệm cốt lõi: - [**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) - [**Paywalls**](paywalls) – cấu hình xác định sản phẩm nào đượ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á và ưu đãi mà không cần chỉnh sửa code. Trong SDK v4, các biến thể paywall cho một placement được mang bởi đối tượng **flow** — bạn fetch một flow và truy vấn các sản phẩm của nó. - [**Placements**](placements) – nơi và thời điểm bạn hiển thị paywall trong app (ví dụ: `main`, `onboarding`, `settings`). Bạn thiết lập paywall cho các placement trên dashboard, sau đó yêu cầu chúng bằng placement ID trong code. Cách này giúp bạn dễ dàng chạy A/B test và hiển thị các paywall khác nhau cho từng nhóm người dùng. Hãy đảm bảo bạn hiểu rõ những khái niệm này ngay cả khi bạn tự xây dựng paywall theo cách riêng. Về cơ bản, đây chỉ là cách bạn quản lý các sản phẩm mà bạn 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 danh sách sản phẩm. Để hiểu những gì cần làm trên dashboard, hãy xem hướng dẫn quickstart [tại đây](quickstart). ### Quản lý người dùng \{#manage-users\} Bạn có thể làm việc với hoặc không có xác thực backend ở phía mình. Tuy nhiên, Adapty SDK xử lý người dùng ẩn danh và người dùng đã xác định theo những cách khác nhau. Đọc [hướng dẫn quickstart về định danh người dùng](flutter-quickstart-identify) để hiểu rõ sự khác biệt và đảm bảo bạn đang làm việc với người dùng đúng cách. ## Bước 1. Lấy danh sách sản phẩm \{#step-1-get-products\} Để lấy danh sách sản phẩm cho paywall tùy chỉnh, bạn cần: 1. Lấy đối tượng `flow` bằng cách truyền ID [placement](placements) vào phương thức `getFlow`. 2. Lấy mảng sản phẩm của flow đó bằng phương thức `getPaywallProducts`. ```dart showLineNumbers Future loadPaywall() async { try { final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID'); final products = await Adapty().getPaywallProducts(flow: flow); // Use products to build your custom paywall UI } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // Handle the error } } ``` ## Bước 2. Xử lý thanh toán \{#step-2-accept-purchases\} Khi người dùng chọn 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 đã chọn. Phương thức này sẽ xử lý luồng thanh toán và trả về hồ sơ người dùng đã được cập nhật. ```dart showLineNumbers Future purchaseProduct(AdaptyPaywallProduct product) async { try { final purchaseResult = await Adapty().makePurchase(product: product); switch (purchaseResult) { case AdaptyPurchaseResultSuccess(profile: final profile): // Purchase successful, profile updated break; case AdaptyPurchaseResultUserCancelled(): // User canceled the purchase break; case AdaptyPurchaseResultPending(): // Purchase is pending (e.g., user will pay offline with cash) break; } } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // Handle the error } } ``` ## Bước 3. Khôi phục giao dịch mua \{#step-3-restore-purchases\} Các cửa hàng ứng dụng 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 mua của họ. Gọi phương thức `restorePurchases` khi người dùng nhấn nút khôi phục. Thao tác này sẽ đồng bộ lịch sử giao dịch mua của họ với Adapty và trả về hồ sơ người dùng đã được cập nhật. ```dart showLineNumbers Future restorePurchases() async { try { final profile = await Adapty().restorePurchases(); // Restore successful, profile updated } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // Handle the error } } ``` ## Bước 4. Kiểm tra trạng thái gói đăng ký \{#step-4-check-the-subscription-status\} Sau khi mua hàng hoặc khôi phục, hãy kiểm tra [mức độ truy cập](access-level) của người dùng để quyết định có hiển thị paywall hay mở khóa tính năng trả phí. Các phương thức `makePurchase` và `restorePurchases` đã trả về hồ sơ người dùng được cập nhật; bất cứ khi nào bạn cần trạng thái hiện tại ở nơi khác trong ứng dụng, hãy dùng phương thức `getProfile`: ```dart showLineNumbers Future hasPremiumAccess() async { try { final profile = await Adapty().getProfile(); return profile.accessLevels['premium']?.isActive ?? false; } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // Handle the error } return false; } ``` Để biết thêm các cách kiểm tra và theo dõi trạng thái gói đăng ký, bao gồm lắng nghe các cập nhật theo thời gian thực, xem [Kiểm tra trạng thái gói đăng ký](flutter-check-subscription-status). ## Các bước tiếp theo \{#next-steps\} :::tip Bạn có câu hỏi hoặc gặp sự cố? Hãy xem [diễn đàn hỗ trợ](https://adapty.featurebase.app/) của chúng tôi — nơi bạn có thể tìm câu trả lời cho các câu hỏi thường gặp hoặc đặt câu hỏi của riêng mình. Đội ngũ và cộng đồng của chúng tôi luôn sẵn sàng giúp đỡ! ::: Paywall của bạn đã sẵn sàng để hiển thị trong ứng dụng. Hãy kiểm tra các giao dịch mua hàng trong [sandbox App Store](test-purchases-in-sandbox) hoặc [Google Play Store](testing-on-android) để đảm bảo bạn có thể hoàn tất một giao dịch mua thử từ paywall. Để xem cách hoạt động này trong một triển khai thực tế, hãy xem [PurchasesObserver](https://github.com/adaptyteam/AdaptySDK-Flutter/blob/master/example/lib/purchase_observer.dart) trong ứng dụng mẫu của chúng tôi — nơi minh họa việc xử lý giao dịch mua hàng với cách xử lý lỗi phù hợp, các UI observer, và tích hợp SDK toàn diện. --- # File: fetch-paywalls-and-products-flutter --- --- title: "Lấy paywall và sản phẩm cho remote config paywalls trong Flutter SDK" description: "Lấy paywall và sản phẩm trong Adapty Flutter SDK để tối ưu hóa doanh thu." --- Trước khi hiển thị Remote Config và custom paywall, bạn cần lấy thông tin về chúng. Lưu ý rằng chủ đề này đề cập đến Remote Config và custom paywall. Để biết hướng dẫn lấy flow và paywall được tùy chỉnh bằng Paywall Builder, vui lòng tham khảo [Lấy flow & paywall](flutter-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 paywall và sản phẩm trong ứng dụng 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 paywall và thêm sản phẩm vào paywall của bạn](create-paywall) trong Adapty Dashboard. 3. [Tạo placement và thêm paywall vào placement](create-placement) trong Adapty Dashboard. 4. [Cài đặt Adapty SDK](sdk-installation-flutter) vào ứng dụng di động 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 paywall, giú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 `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 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 là ba, hãy hiển thị tất cả mà không cần thay đổi code. ::: ```dart showLineNumbers try { final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID'); // the requested flow } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // 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ừ server 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 vấn đề về kết nối mạng, 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ó kém đế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 phiên để 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 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](flutter-use-fallback-paywalls). Chúng tôi cũng sử dụng CDN để tải paywall nhanh hơn và một server 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 internet kém.

| | **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ị đã 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.

| :::note Trong v4, `getFlow` không nhận tham số `locale`. Đối với paywall tùy chỉnh, tất cả các bản dịch có sẵn đều được trả về trong Remote Config của flow (`flow.remoteConfigs`) — chọn bản phù hợp với thiết bị hoặc cài đặt ngôn ngữ của người dùng. Xem [Localizations và mã locale](flutter-localizations-and-locale-codes). ::: Các tham số trả về: | Parameter | Description | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Flow | Một đối tượng `AdaptyFlow` chứa các định danh của flow (`instanceIdentity`, `variationId`), tên, placement, các biến thể paywall (`paywalls`), và các Remote Config (`remoteConfigs`). | ## Lấy danh sách 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ó: ```dart showLineNumbers try { final products = await Adapty().getPaywallProducts(flow: flow); // the requested products array } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` Tham số trả về: | Tham số | Mô tả | | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Products | Danh sách các đối tượng [`AdaptyPaywallProduct`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywallProduct-class.html) 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 triển khai thiết kế paywall của riêng bạn, bạn có thể sẽ cần truy cập các thuộc tính từ đối tượng [`AdaptyPaywallProduct`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywallProduct-class.html). Dưới đây là các thuộc tính được sử dụng phổ biến nhất, nhưng hãy tham khảo 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 hiện có. | Thuộc tính | Mô tả | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Tiêu đề** | Để hiển thị tiêu đề 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, chứ 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.price.localizedString`. Việc 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ố thông qua `product.price.amount`. Giá trị sẽ được trả về theo đơn vị tiền tệ địa phương. Để lấy ký hiệu tiền tệ tương ứng, dùng `product.price.currencySymbol`. | | **Chu kỳ gói đăng ký** | Để hiển thị chu kỳ (ví dụ: tuần, tháng, năm, v.v.), dùng `product.subscription?.localizedPeriod`. Việc 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 cách lập trình, dùng `product.subscription?.period`. Từ đó bạn có thể truy cập enum `unit` để lấy độ dài (tức là day, week, month, year hoặc unknown). Giá trị `numberOfUnits` cho biết số đơn vị chu kỳ. Ví dụ: với gói đăng ký theo quý, thuộc tính unit sẽ là `AdaptyPeriodUnit.month` và numberOfUnits sẽ là `3`. | | **Ưu đãi giới thiệu** | Để hiển thị huy hiệu hoặc chỉ báo cho biết gói đăng ký có ưu đãi giới thiệu, hãy kiểm tra thuộc tính `product.subscription?.offer?.phases`. Đây là danh sách có thể chứa tối đa hai giai đoạn giảm giá: giai đoạn dùng thử miễn phí và giai đoạn giá ưu đãi giới thiệu. Trong mỗi đối tượng phase có các thuộc tính hữu ích sau:
• `paymentMode`: một enum với các giá trị `AdaptyPaymentMode.freeTrial`, `AdaptyPaymentMode.payAsYouGo`, `AdaptyPaymentMode.payUpFront` và `AdaptyPaymentMode.unknown`. Dùng thử miễn phí sẽ có kiểu `AdaptyPaymentMode.freeTrial`.
• `price`: Giá giảm dưới dạng số. Với dùng thử miễn phí, giá trị này sẽ là `0`.
• `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 từng phần của chu kỳ ưu đãi bằng 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ủa ưu đãi đã được định dạng theo ngôn ngữ của người dùng. | ## 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ối ưu 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ó kết nối internet yếu, việc tải flow 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 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 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ư được mô tả chi tiết trong phần [Lấy thông tin flow](fetch-paywalls-and-products-flutter#fetch-flow-information) ở trên. :::warning Tại sao 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 đề 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 hiển thị được. - **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 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` đã được mô tả [ở trên](fetch-paywalls-and-products-flutter#fetch-flow-information). ::: ```dart showLineNumbers try { final flow = await Adapty().getFlowForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID'); // the requested flow } on AdaptyError catch (adaptyError) { // handle error } catch (e) { // handle unknown 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ừ 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 hay gặp tình trạng mất mạng, 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 tốc độ tải sẽ nhanh hơn bất kể kết nối mạng có kém đế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.

|
Trước khi hiển thị Remote Config và 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 cách lấy paywall cho các paywall được tùy chỉnh bằng Paywall Builder, vui lòng tham khảo [Lấy paywall Paywall Builder và cấu hình của chúng](flutter-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 paywall và sản phẩm trong ứng dụng di động của bạn (nhấp để mở rộng) 1. [Tạo sản phẩm của bạn](create-product) trong Adapty Dashboard. 2. [Tạo paywall và đưa các sản phẩm vào paywall của bạn](create-paywall) trong Adapty Dashboard. 3. [Tạo placement và đưa paywall vào placement](create-placement) trong Adapty Dashboard. 4. [Cài đặt Adapty SDK](sdk-installation-flutter) vào ứng dụng di động 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 mình bằng phương thức `getPaywall`. :::important **Đừng hardcode product ID.** ID duy nhất bạn cầ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 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 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. ::: ```dart showLineNumbers try { final paywall = await Adapty().getPaywall(placementId: "YOUR_PLACEMENT_ID", locale: "en"); // the requested paywall } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` | 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 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` là tiếng Anh, `pt-br` là tiếng Bồ Đào Nha của Brazil.

Xem [Bản địa hóa và mã locale](flutter-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ị 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 kết nối 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ủa họ ra sao. Cache đượ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 để 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 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](flutter-use-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 mạng kém.

| | **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 ID sản phẩm! Vì các 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 (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 tình huống này. Ví dụ: nếu ban đầu bạn nhận được 2 sản phẩm, ứng dụng của bạn nên hiển thị 2 sản phẩm đó. Tuy nhiên, nếu sau này bạn nhận được 3 sản phẩm, ứng dụng của bạn nên hiển thị cả 3 mà không cần thay đổi bất kỳ dòng code nào. Thứ duy nhất bạn phải hardcode là placement ID. Các tham số trả về: | Tham số | Mô tả | | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | Một đối tượng [`AdaptyPaywall`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html) bao 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 danh sách 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 nó: ```dart showLineNumbers try { final products = await Adapty().getPaywallProducts(paywall: paywall); // the requested products array } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Các tham số phản hồi: | Tham số | Mô tả | | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Products | Danh sách các đối tượng [`AdaptyPaywallProduct`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywallProduct-class.html) 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://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywallProduct-class.html). Dưới đây là các thuộc tính thường dùng 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 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, chứ 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.price.localizedString`. Việc 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.amount`. 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.price.currencySymbol`. | | **Chu kỳ gói đăng ký** | Để hiển thị chu kỳ (ví dụ: tuần, tháng, năm, v.v.), sử dụng `product.subscription?.localizedPeriod`. Việc 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 cách lập trình, sử dụng `product.subscription?.period`. 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 biết số đơn vị chu kỳ. Ví dụ, với gói đăng ký theo quý, bạn sẽ thấy `AdaptyPeriodUnit.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ỉ báo rằng gói đăng ký có ưu đãi giới thiệu, hãy kiểm tra thuộc tính `product.subscription?.offer?.phases`. Đây là danh sách có thể chứa tối đa hai giai đoạn giảm giá: giai đoạn dùng thử miễn phí và giai đoạn giá giới thiệu. Trong mỗi đối tượng giai đoạn có các thuộc tính hữu ích sau:
• `paymentMode`: một enum với các giá trị `AdaptyPaymentMode.freeTrial`, `AdaptyPaymentMode.payAsYouGo`, `AdaptyPaymentMode.payUpFront` và `AdaptyPaymentMode.unknown`. Bản dùng thử miễn phí sẽ có kiểu `AdaptyPaymentMode.freeTrial`.
• `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`.
• `localizedNumberOfPeriods`: một 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ử 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 riêng lẻ của chu kỳ ưu đãi bằng thuộc tính này. Nó hoạt động theo cách 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. | ## Tăng tốc độ tải paywall bằng paywall mặc định cho đối tượng \{#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 các trường hợp bạn có nhiều đối tượng và paywall, hoặc người dùng có 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 như vậy, 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ị 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 tải 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à tải paywall bằng phương thức `getPaywall`, như đã trình bày chi tiết trong phần [Lấy thông tin Paywall](fetch-paywalls-and-products-flutter#fetch-paywall-information) ở 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ể: - **Các 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 có 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 đi 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 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-flutter#fetch-paywall-information). ::: ```dart showLineNumbers try { final paywall = await Adapty().getPaywallForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID'); } on AdaptyError catch (adaptyError) { // handle error } catch (e) { // handle unknown error } ``` :::note Phương thức `getPaywallForDefaultAudience` khả dụng từ Flutter SDK phiên bản 3.2.0 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 địa hóa paywall](add-remote-config-locale). Tham số này là mã ngôn ngữ gồm một hoặc nhiều subtag được phân tách bằng dấu gạch ngang (**-**). Subtag đầu tiên là mã ngôn ngữ, subtag thứ hai là mã 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](flutter-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 đã lưu trong bộ nhớ đệm 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 kết nối internet không ổn định, hãy cân nhắc sử dụng `.returnCacheDataElseLoad` để trả về dữ liệu đã lưu trong 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, bất kể chất lượng kết nối internet của họ. Bộ nhớ đệm được cập nhật thường xuyên, nên có thể 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 bộ nhớ đệm 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 dọn dẹp thủ công.

|
--- # File: present-remote-config-paywalls-flutter --- --- title: "Hiển thị paywall được thiết kế bằng Remote Config trong Flutter SDK" description: "Khám phá cách hiển thị paywall Remote Config trong Adapty Flutter SDK để 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 có thể thấy nó. Vì Remote Config linh hoạt theo nhu cầu của bạn, bạn toàn quyền quyết định nội dung hiển thị và giao diện của paywall. Chúng tôi cung cấp một phương thức để lấy cấu hình remote, giúp bạn chủ động hiển thị paywall tùy chỉnh đã được thiết lập qua Remote Config. ## Lấy Remote Config của paywall và hiển thị \{#get-paywall-remote-config-and-present-it\} Trong v4, flow chứa danh sách `remoteConfigs` — mỗi remote config tương ứng với một ngôn ngữ đã được cấu hình. Chọn mục phù hợp với ngôn ngữ của người dùng và trích xuất các giá trị cần thiết. Xem [Bản địa hóa và mã ngôn ngữ](flutter-localizations-and-locale-codes) để chọn đúng ngôn ngữ. ```dart showLineNumbers try { final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID'); // one entry per configured localization; fall back to the first one final config = flow.remoteConfigs.firstWhereOrNull((c) => c.locale == 'en') ?? flow.remoteConfig; final String? headerText = config?.dictionary?['header_text'] as String?; } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` Tại thời điểm này, sau khi đã nhận được tất cả các giá trị cần thiết, đã đến lúc render và kết hợp chúng thành một trang có giao diện đẹp. Hãy đảm bảo thiết kế tương thích với nhiều loại màn hình điện thoại và hướng xoay khác nhau, mang lại trải nghiệm mượt mà và thân thiện với người dùng trên mọi thiết bị. :::warning Hãy nhớ ghi lại sự kiện xem paywall như mô tả bên dưới, để Adapty analytics có thể thu thập thông tin cho các funnel và A/B test. ::: Sau khi hiển thị paywall xong, hãy 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](flutter-making-purchases). Chúng tôi khuyến nghị [tạo một paywall dự phòng](flutter-use-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 chúng tôi tự động thu thập dữ liệu về các giao dịch mua, việc ghi lại lượt xem paywall cần có sự can thiệp của bạn, vì chỉ bạn mới biết khi nào khách hàng nhìn thấy paywall. Để ghi lại sự kiện xem paywall, chỉ cần gọi `.logShowFlow(flow: flow)`, và nó sẽ được phản ánh trong các chỉ số paywall của bạn trong funnels và A/B test. :::important Không cần gọi `.logShowFlow(flow: flow)` nếu bạn đang hiển thị các flow hoặc paywall được tạo trong [builder](adapty-paywall-builder). ::: ```dart showLineNumbers try { await Adapty().logShowFlow(flow: flow); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :---------- | :------- |:----------------------------------------------------------------------| | **flow** | bắt buộc | Một đối tượng `AdaptyFlow`. | 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 có thể thấy nó. Vì Remote Config cho phép linh hoạt theo nhu cầu của bạn, bạn hoàn toàn quyết định những gì được đưa vào và giao diện paywall trông như thế nào. Chúng tôi cung cấp một phương thức để lấy cấu hình Remote Config, giúp bạn chủ động hiển thị paywall tùy chỉnh đã được thiết lập qua Remote Config. ## 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, hãy truy cập thuộc tính `remoteConfig` và trích xuất các giá trị cần thiết. ```dart showLineNumbers try { final paywall = await Adapty().getPaywall(placementId: "YOUR_PLACEMENT_ID"); final String? headerText = paywall.remoteConfig?.dictionary?['header_text'] as String?; } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Tại thời điểm này, sau khi đã nhận được tất cả các giá trị cần thiết, đã đến lúc render và kết hợp chúng thành một trang có giao diện hấp dẫn. Hãy đảm bảo thiết kế phù hợp với nhiều loại màn hình điện thoại và các hướng xoay khác nhau, mang lại trải nghiệm mượt mà và thân thiện với người dùng trên mọi thiết bị. :::warning Hãy đảm bảo ghi lại sự kiện xem paywall như mô tả bên dưới, cho phép Adapty analytics thu thập thông tin cho các funnel và A/B test. ::: Sau khi hiển thị paywall xong, hãy 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](flutter-making-purchases). Chúng tôi khuyến nghị [tạo một paywall dự phòng](flutter-use-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 suất của các paywall. Trong khi chúng tôi tự động thu thập dữ liệu về giao dịch mua hàng, việc ghi lại lượt xem paywall cần có sự tham gia của bạn vì chỉ bạn mới biết khi nào người dùng nhìn thấy một paywall. Để ghi lại sự kiện xem paywall, chỉ cần gọi `.logShowPaywall(paywall)`, và nó sẽ được phản ánh trong các chỉ số paywall của bạn trong funnels và A/B test. :::important Không cần gọi `.logShowPaywall(paywall)` nếu bạn đang hiển thị các paywall được tạo trong [Paywall Builder](adapty-paywall-builder). ::: ```dart showLineNumbers try { final result = await Adapty().logShowPaywall(paywall: paywall); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Tham số yêu cầu: | Tham số | Bắt buộc | Mô tả | | :---------- | :------- |:----------------------------------------------------------------------| | **paywall** | required | Một đối tượng [`AdaptyPaywall`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html). | --- # File: flutter-making-purchases --- --- title: "Thực hiện mua hàng trong ứng dụng di động với Flutter 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 di độ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ỉ hiển thị paywall thôi 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 sử dụng Paywall Builder, bạn phải dùng một phương thức riêng gọi là `.makePurchase()` để hoàn tất giao dịch mua 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 các paywall và thực hiện giao dịch của họ. Nếu paywall của bạn có ưu đãi đang hoạt động cho sản phẩm mà người dùng muốn mua, Adapty sẽ tự động áp dụng ưu đãi đó tại thời điểm thanh toán. :::warning Lưu ý rằng ưu đãi giới thiệu sẽ chỉ được áp dụng tự động nếu bạn sử dụng các paywall được thiết lập bằng Paywall Builder. Trong các trường hợp khác, bạn cần [kiểm tra đ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ể khiến ứng dụng của bạn bị từ chối khi phát hành. Hơn nữa, người dùng đủ điều kiện nhận ưu đãi giới thiệu có thể bị tính giá đầy đủ. ::: Hãy đảm bảo bạn đã [thực hiện 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 các giao dịch mua. ## Thực hiện mua hàng \{#make-purchase\} :::note **Đang dùng [Paywall Builder](adapty-paywall-builder)?** Các giao dịch mua đượ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](flutter-implement-paywalls-manually) để biết hướng dẫn triển khai đầy đủ từ đầu đến cuối. ::: ```dart showLineNumbers try { final purchaseResult = await Adapty().makePurchase(product: product); switch (purchaseResult) { case AdaptyPurchaseResultSuccess(profile: final profile): if (profile.accessLevels['premium']?.isActive ?? false) { // Grant access to the paid features } break; case AdaptyPurchaseResultPending(): break; case AdaptyPurchaseResultUserCancelled(): break; default: break; } } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // 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://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywallProduct-class.html) được 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://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyProfile-class.html) cung cấp thông tin đầy đủ về mức độ truy cập, các 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 đ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 deprecate. ::: ## Thay đổi gói đăng ký khi mua hàng \{#change-subscription-when-making-a-purchase\} Khi người dùng chọn một gói đăng ký mới thay vì gia hạn gói hiện tại, cách thức hoạt động sẽ phụ thuộc vào cửa hàng: - Với App Store, gói đăng ký sẽ tự động được cập nhật trong nhóm gói đăng ký. Nếu người dùng mua một gói đăng ký từ nhóm này trong khi đã có gói đăng ký từ nhóm khác, cả hai gói sẽ cùng hoạt động đồng thời. - Với Google Play, gói đăng ký không tự động được cập nhật. Bạn cần xử lý việc chuyển đổi trong code của ứng dụng như mô tả bên dưới. Để thay thế gói đăng ký bằng một gói khác trên Android, hãy gọi phương thức `.makePurchase()` với tham số bổ sung: ```dart showLineNumbers try { final subscriptionUpdateParams = AdaptyAndroidSubscriptionUpdateParameters( 'OLD_PRODUCT_ID', AdaptyAndroidSubscriptionUpdateReplacementMode.immediateWithTimeProration, ); final result = await Adapty().makePurchase( product: product, parameters: AdaptyPurchaseParameters( subscriptionUpdateParams: subscriptionUpdateParams, ), ); // successful cross-grade } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // Handle the error } ``` Tham số yêu cầu bổ sung: | Parameter | Presence | Description | | :--------------------------- | :------- |:--------------------------------------------------------------------------------------------------------| | **parameters** | required | một đối tượng `AdaptyPurchaseParameters` với trường `subscriptionUpdateParams` được đặt thành một đối tượng [`AdaptyAndroidSubscriptionUpdateParameters`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyAndroidSubscriptionUpdateParameters-class.html). | Bạn có thể đọc thêm về gói đăng ký và các chế độ thay thế trong tài liệu dành cho nhà phát triển của Google: - [Về các chế độ thay thế](https://developer.android.com/google/play/billing/subscriptions#replacement-modes) - [Đề xuất từ Google về các chế độ thay thế](https://developer.android.com/google/play/billing/subscriptions#replacement-recommendations) - Chế độ thay thế [`CHARGE_PRORATED_PRICE`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#CHARGE_PRORATED_PRICE()). Lưu ý: phương thức này chỉ áp dụng cho việc nâng cấp gói đăng ký. Hạ cấp không được hỗ trợ. - Chế độ thay thế [`DEFERRED`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.SubscriptionUpdateParams.ReplacementMode#DEFERRED()). Lưu ý: Việc thay đổi gói đăng ký thực sự chỉ xảy ra khi chu kỳ thanh toán hiện tại kết thúc. ## Đổi mã ưu đãi trong iOS \{#redeem-offer-codes-in-ios\}
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\} :::warning 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ị màn hình nhập mã trong ứng dụng của bạn: ```dart showLineNumbers try { await Adapty().presentCodeRedemptionSheet(); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` :::danger Theo quan sát của chúng tôi, màn hình đổi mã ưu đãi 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. Để thực hiện đ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}` ::: ### Quản lý gói trả trước (Android) \{#manage-prepaid-plans-android\} Nếu người dùng ứng dụng của bạn có thể mua [gói trả trước](https://developer.android.com/google/play/billing/subscriptions#prepaid-plans) (ví dụ: mua gói đăng ký không tự gia hạn trong vài tháng), bạn có thể bật [giao dịch chờ xử lý](https://developer.android.com/google/play/billing/subscriptions#pending) cho các gói trả trước. ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withGoogleEnablePendingPrepaidPlans(true), ); ``` --- # File: flutter-restore-purchase --- --- title: "Khôi phục giao dịch mua trong ứng dụng di động với Flutter 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 trên cả iOS và Android 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 đó, chẳng hạn như các gói đăng ký hoặc in-app purchase, mà không bị tính phí thêm. 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 lại ứng dụng, hoặc chuyển sang thiết bị mới và muốn truy cập nội dung đã mua trước đó mà không phải thanh toán lại. :::note Trong các paywall được xây dựng bằng [Paywall Builder](adapty-paywall-builder), giao dịch mua được khôi phục tự động mà không cần thêm code từ 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()`: ```dart showLineNumbers try { final profile = await Adapty().restorePurchases(); if (profile?.accessLevels['YOUR_ACCESS_LEVEL']?.isActive ?? false) { // successful access restore } } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Tham số phản hồi: | Tham số | Mô tả | |---------|-----------| | **Profile** |

Một đối tượng [`AdaptyProfile`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyProfile-class.html). Model này chứa thông tin về mức độ truy cập, 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: implement-observer-mode-flutter --- --- title: "Triển khai Observer mode trong Flutter 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 Flutter SDK." --- Nếu bạn đã có hệ thống xử lý in-app purchase 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 phân tích nâng cao và tích hợp liền mạch với các hệ thống attribution và phân tích. Nếu cách này phù hợp với 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`. Làm theo hướng dẫn cài đặt cho [Flutter](sdk-installation-flutter#activate-adapty-module-of-adapty-sdk). 2. [Báo cáo giao dịch](report-transactions-observer-mode-flutter) từ hệ thống mua hàng hiện tại của bạn lên Adapty. ## Thiết lập chế độ Observer \{#observer-mode-setup\} Bật chế độ Observer nếu bạn tự xử lý các giao dịch mua và trạng thái gói đăng ký, và chỉ dùng Adapty để gửi sự kiện gói đăng ký và phân tích. :::important Khi chạy ở chế độ Observer, 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 này. ::: ```dart showLineNumbers title="main.dart" await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') ..withObserverMode(true) // Enable observer mode ..withLogLevel(AdaptyLogLevel.verbose), ); ``` Tham số: | Tham số | Mô tả | | --------------------------- | ------------------------------------------------------------ | | observerMode | Giá trị boolean kiểm soát [Chế độ Observer](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êm một số bước cấu hình trong Observer mode. Ngoài các bước đã thực hiện ở trên, bạn cần làm thêm: 1. Hiển thị paywall như thông thường đối với [Remote Config paywall](present-remote-config-paywalls-flutter). 3. [Liên kết paywall](report-transactions-observer-mode-flutter) với các giao dịch mua hàng. :::tip Trong SDK v4, bạn cũng có thể hiển thị các flow và paywall được Adapty render trong Observer mode: đăng ký một `AdaptyUIObserverModeResolver` để thực hiện giao dịch mua hoặc khôi phục bằng code của chính bạn khi người dùng nhấn vào nút tương ứng. Xem [Hiển thị flow trong Observer mode](flutter-present-flows-in-observer-mode). ::: --- # File: report-transactions-observer-mode-flutter --- --- title: "Báo cáo giao dịch ở chế độ Observer Mode trong Flutter 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 trong Flutter SDK." --- Trong Observer mode, Adapty SDK không thể tự theo dõi các giao dịch mua được thực hiện qua hệ thống mua hàng 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 sai sót trong phân tích. Sử dụng `reportTransaction` để báo cáo từng giao dịch một cách rõ ràng để Adapty nhận biết. :::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 ra giao dịch đó, nó 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 đính kè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. ```dart showLineNumbers try { // every time when calling transaction.finish() await Adapty().reportTransaction( "YOUR_TRANSACTION_ID", variationId: "PAYWALL_VARIATION_ID", // optional ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` Tham số: | Parameter | Presence | Description | | ------------- | -------- | ------------------------------------------------------------ | | transactionId | required |
  • Đối với iOS: Mã định danh của giao dịch.
  • Đối với Android: Mã định danh dạng chuỗi `purchase.getOrderId` của giao dịch mua, trong đó purchase là một instance của lớp [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) trong billing library.
| | variationId | optional | Mã định danh dạng chuỗi của biến thể. Bạn có thể lấy nó thông qua thuộc tính `variationId` của đối tượng [AdaptyPaywall](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html). |
Trong Observer mode, Adapty SDK không thể tự theo dõi các giao dịch mua được thực hiện qua hệ thống mua hàng 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` trên cả hai nền tảng để báo cáo từng giao dịch một cách rõ ràng, và sử dụng `restorePurchases` trên Android như một bước bổ sung để đảm bảo Adapty nhận ra giao dịch đó. :::warning **Đừng bỏ qua bước báo cáo giao dịch và khôi phục mua hàng!** Nếu bạn không gọi các phương thức này, Adapty sẽ không nhận ra 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 truyền `variationId` khi báo cáo giao dịch. Điều này liên kết giao dịch mua với paywall đã kích hoạt nó, đảm bảo analytics paywall chính xác. ```dart showLineNumbers // every time when calling transaction.finish() if (Platform.isAndroid) { try { await Adapty().restorePurchases(); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } } try { // every time when calling transaction.finish() await Adapty().reportTransaction( "YOUR_TRANSACTION_ID", variationId: "PAYWALL_VARIATION_ID", // optional ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` Tham số: | Tham số | Bắt buộc | Mô tả | | ------------- | -------- | ------------------------------------------------------------ | | transactionId | bắt buộc |
  • Với iOS, StoreKit 1: một đối tượng [SKPaymentTransaction](https://developer.apple.com/documentation/storekit/skpaymenttransaction).
  • Với iOS, StoreKit 2: đối tượng [Transaction](https://developer.apple.com/documentation/storekit/transaction).
  • Với Android: Chuỗi định danh (purchase.getOrderId của giao dịch mua, trong đó purchase là một instance của lớp [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) thuộc billing library.
| | variationId | tùy chọn | Chuỗi định danh của biến thể. Bạn có thể lấy nó thông qua thuộc tính `variationId` của đối tượng [AdaptyPaywall](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html). |
**Báo cáo giao dịch** - Các phiên bản đến 3.1.x tự động lắng nghe các giao dịch trên App Store, do đó không cần báo cáo thủ công. - Phiên bản 3.2 không hỗ trợ Observer Mode. **Báo cáo giao dịch** Dùng `restorePurchases` để báo cáo giao dịch cho Adapty trong Observer Mode, như được giải thích trên trang [Khôi phục giao dịch mua trong code mobile](flutter-restore-purchase). :::warning **Đừng bỏ qua bước báo cáo giao dịch!** Nếu bạn không gọi `restorePurchases`, Adapty sẽ không nhận ra giao dịch đó, nó sẽ không xuất hiện trong analytics và sẽ không được gửi đến các tích hợp. ::: **Liên kết paywall với giao dịch** SDK của Adapty không thể xác định nguồn gốc của các giao dịch mua, vì bạn là người xử lý chúng. Do đó, nếu bạn dự định sử dụng paywall và/hoặc A/B test trong chế độ Observer, bạn cần liên kết giao dịch đến từ cửa hàng ứng dụng với paywall tương ứng trong code ứng dụng di động của mình. Điều này cần được thực hiện đúng trước khi phát hành ứng dụng, nếu không sẽ dẫn đến sai sót trong phân tích. ```dart final transactionId = transaction.transactionIdentifier final variationId = paywall.variationId try { await Adapty().setVariationId('transactionId', variationId); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ```
--- # File: flutter-troubleshoot-purchases --- --- title: "Xử lý sự cố mua hàng trong Flutter SDK" description: "Xử lý sự cố mua hàng trong Flutter 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 Flutter SDK. ## makePurchase được gọi thành công nhưng hồ sơ người dùng không được cập nhật \{#makepurchase-is-called-successfully-but-the-profile-is-not-being-updated\} **Vấn đề**: Phương thức `makePurchase` hoàn tất thành công, nhưng hồ sơ người dùng và trạng thái gói đăng ký không được cập nhật trong Adapty. **Nguyên nhân**: Điều này thường cho thấy quá trình thiết lập Google Play Store chưa hoàn chỉnh hoặc có vấn đề về cấu hình. **Giải pháp**: Đảm bảo bạn đã hoàn thành tất cả [các bước thiết lập Google Play](initial-android). ## makePurchase được gọi hai lần \{#makepurchase-is-invoked-twice\} **Vấn đề**: Phương thức `makePurchase` đang được gọi nhiều lần cho cùng một giao dịch mua. **Nguyên nhân**: Điều này thường xảy ra khi flow mua hàng được kích hoạt nhiều lần do vấn đề quản lý trạng thái giao diện hoặc người dùng thao tác quá nhanh. **Giải pháp**: Đảm bảo bạn đã hoàn thành tất cả [các bước thiết lập Google Play](initial-android). ## AdaptyError.cantMakePayments ở chế độ observer \{#adaptyerrorcantmakepayments-in-observer-mode\} **Vấn đề**: Bạn nhận được `AdaptyError.cantMakePayments` khi sử dụng `makePurchase` ở chế độ observer. **Nguyên nhân**: Ở chế độ observer, bạn nên tự xử lý giao dịch mua ở phía mình, không sử dụng phương thức `makePurchase` của Adapty. **Giải pháp**: Nếu bạn dùng `makePurchase` cho giao dịch mua, hãy tắt chế độ observer. Bạn cần chọn một trong hai: dùng `makePurchase` hoặc tự xử lý giao dịch mua ở phía mình trong chế độ observer. Xem [Triển khai chế độ Observer](implement-observer-mode-flutter) để biết thêm chi tiết. ## Lỗi Adapty: (code: 103, message: Play Market request failed on purchases updated: responseCode=3, debugMessage=Billing Unavailable, detail: null) \{#adapty-error-code-103-message-play-market-request-failed-on-purchases-updated-responsecode3-debugmessagebilling-unavailable-detail-null\} **Vấn đề**: Bạn nhận được lỗi billing không khả dụng từ Google Play Store. **Nguyên nhân**: Lỗi này không liên quan đến Adapty. Đây là lỗi từ Google Play Billing Library cho biết tính năng thanh toán không khả dụng trên thiết bị. **Giải pháp**: Lỗi này không liên quan đến Adapty. Bạn có thể tìm hiểu thêm trong tài liệu Play Store: [Handle BillingResult response codes](https://developer.android.com/google/play/billing/errors#billing_unavailable_error_code_3) | Play Billing | Android Developers. ## Không tìm thấy makePurchasesCompletionHandlers \{#not-found-makepurchasescompletionhandlers\} **Vấn đề**: Bạn gặp sự cố với `makePurchasesCompletionHandlers` không được tìm thấy. **Nguyên nhân**: Vấn đề này thường liên quan đến sự cố khi kiểm thử trong môi trường sandbox. **Giải pháp**: Tạo một người dùng sandbox mới và thử lại. Cách này thường giải quyết được các vấn đề với purchase completion handler trong sandbox. ## Các vấn đề khác \{#other-issues\} **Vấn đề**: Bạn đang gặp các sự cố liên quan đến mua hàng khác chưa đượ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](flutter-sdk-migration-guides) nếu cần. Nhiều vấn đề đã được giải quyết trong các phiên bản SDK mới hơn. --- # File: flutter-identifying-users --- --- title: "Xác định người dùng trong Flutter SDK" description: "Xác định người dùng trong Adapty để cải thiện trải nghiệm đă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 của họ trong phần [Profiles](profiles-crm) và sử dụng nó trong [server-side API](getting-started-with-server-side-api), thứ sẽ được gửi đến tất cả các tích hợp. ### Đặt Customer User ID khi cấu hình \{#setting-customer-user-id-on-configuration\} Nếu bạn đã có user ID khi cấu hình, hãy truyền nó qua tham số `customerUserId` vào phương thức `.activate()`: ```dart showLineNumbers title="Dart" try { await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_API_KEY') ..withCustomerUserId(YOUR_CUSTOMER_USER_ID) ); } catch (e) { // 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 \{#setting-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ó bất cứ lúc nào sau đó bằng phương thức `.identify()`. Trường hợp phổ biến nhất để sử 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ừ người dùng ẩn danh sang người dùng đã xác thực. ```dart showLineNumbers try { await Adapty().identify(customerUserId); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` 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 của họ, máy chủ Adapty đã có thông tin về người dùng đó. Trong những tình huống 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 nào đó 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 đã được xác định. Ngoài ra, bạn cần gọi lại tất cả cá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 cứ lúc nào bằng cách gọi phương thức `.logout()`: ```dart showLineNumbers try { await Adapty().logout(); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle unknown error } ``` Sau đó bạn có thể đăng nhập người dùng bằng phương thức `.identify()`. ## Gán `appAccountToken` (iOS) \{#assign-appaccounttoken-ios\} [`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 liên kết token này với mọi giao dịch, giúp backend của bạn 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 từng người dùng và tái sử dụng nó cho cùng một tài khoản trên nhiều thiết bị. Điều này đảm bảo rằng các giao dịch mua và thông báo từ App Store được liên kết đúng cách. Bạn có thể đặt token theo hai cách – trong quá trình kích hoạt 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 bạn chỉ truyền token, nó sẽ không được đưa vào giao dịch. ::: ```dart showLineNumbers // During configuration: try { await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_API_KEY') ..withCustomerUserId(YOUR_CUSTOMER_USER_ID, iosAppAccountToken: "YOUR_APP_ACCOUNT_TOKEN") ); } catch (e) { // handle the error } // Or when identifying users try { await Adapty().identify(customerUserId, iosAppAccountToken: "YOUR_APP_ACCOUNT_TOKEN"); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ### Đặt obfuscated account ID (Android) \{#set-obfuscated-account-ids-android\} Google Play yêu cầu obfuscated account ID cho một số trường hợp sử dụng nhằm tăng cường quyền riêng tư và bảo mật người dùng. Các ID này giúp Google Play xác định các giao dịch mua trong khi vẫn giữ thông tin người dùng ẩn danh, điều này đặc biệt quan trọng để phòng chống gian lận và phân tích. Bạn có thể cần đặt các ID này nếu ứng dụng của bạn xử lý dữ liệu người dùng nhạy cảm hoặc nếu bạn cần tuân thủ các quy định về quyền riêng tư cụ thể. Các obfuscated ID cho phép Google Play theo dõi các giao dịch mua mà không để lộ định danh người dùng thực tế. ```dart showLineNumbers // During configuration: try { await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_API_KEY') ..withCustomerUserId(YOUR_CUSTOMER_USER_ID, androidObfuscatedAccountId: "OBFUSCATED_ACCOUNT_ID") ); } catch (e) { // handle the error } // Or when identifying users try { await Adapty().identify(customerUserId, androidObfuscatedAccountId: "OBFUSCATED_ACCOUNT_ID"); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ## Phát hiện người dùng trên nhiều thiết bị \{#detect-users-across-devices\} 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: flutter-setting-user-attributes --- --- title: "Thiết lập thuộc tính người dùng trong Flutter 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 tốt 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 ứng dụng. Sau đó, bạn có thể 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. ### Cài đặt thuộc tính người dùng \{#setting-user-attributes\} Để cài đặt thuộc tính người dùng, gọi phương thức `.updateProfile()`: ```dart showLineNumbers final builder = AdaptyProfileParametersBuilder() ..setEmail("email@email.com") ..setPhoneNumber("+18888888888") ..setFirstName('John') ..setLastName('Appleseed') ..setGender(AdaptyProfileGender.other) ..setBirthday(DateTime(1970, 1, 3)); try { await Adapty().updateProfile(builder.build()); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Xin lưu ý rằng các thuộc tính bạn đã đặt 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 key được phép \{#the-allowed-keys-list\} Các key `` đượ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ị cho phép: `female`, `male`, `other` | | birthday | Date | ### Thuộc tính người dùng tùy chỉnh \{#custom-user-attributes\} Bạn có thể tự thiết lập các thuộc tính tùy chỉnh cho người dùng. 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 của bạn. Ví dụ, với ứng dụng thể dục, đó có thể là số buổi tập mỗi tuần; với ứng dụng học ngoại 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 nhắm mục tiêu, đồng thời cũng có thể dùng trong analytics để xác định những chỉ số sản phẩm nào ảnh hưởng nhiều nhất đến doanh thu. ```dart showLineNumbers try { final builder = AdaptyProfileParametersBuilder() ..setCustomStringAttribute('value1', 'key1') ..setCustomDoubleAttribute(1.0, 'key2'); await Adapty().updateProfile(builder.build()); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Để xóa key hiện có, sử dụng phương thức `.withRemoved(customAttributeForKey:)`: ```dart showLineNumbers try { final builder = AdaptyProfileParametersBuilder() ..removeCustomAttribute('key1') ..removeCustomAttribute('key2'); await Adapty().updateProfile(builder.build()); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Đô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òn cập nhật, vì các 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, do đó các thuộc tính trên server 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 key tối đa 30 ký tự. Tên key có thể bao 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: flutter-listen-subscription-changes --- --- title: "Kiểm tra trạng thái gói đăng ký trong Flutter SDK" description: "Theo dõi và quản lý trạng thái gói đăng ký người dùng trong Adapty để cải thiện việc giữ chân khách hàng trong ứng dụng Flutter của bạn." --- 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 chèn 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ý (Nhấn để mở rộng) - Với iOS, hãy thiết lập [App Store Server Notifications](enable-app-store-server-notifications) - Với Android, hãy thiết lập [Real-time Developer Notifications (RTDN)](enable-real-time-developer-notifications-rtdn)
## Mức độ truy cập và đối tượng AdaptyProfile \{#access-level-and-the-adaptyprofile-object\} Mức độ truy cập là các thuộc tính của đối tượng [AdaptyProfile](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyProfile-class.html). Chúng tôi khuyến nghị bạn 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](flutter-identifying-users#setting-customer-user-id-on-configuration), rồi cập nhật lại mỗi khi có thay đổi. Bằng cách này, bạn có thể sử dụng đối tượng hồ sơ mà không cần liên tục gửi yêu cầu lấy lại. Để nhận thông báo về các cập nhật hồ sơ người dùng, hãy lắng nghe các thay đổi hồ sơ như được mô tả trong phần [Lắng nghe cập nhật hồ sơ, bao gồm mức độ truy cập](flutter-listen-subscription-changes) 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ừ máy chủ \{#retrieving-the-access-level-from-the-server\} Để lấy mức độ truy cập từ máy chủ, sử dụng phương thức `.getProfile()`: ```dart showLineNumbers try { final profile = await Adapty().getProfile(); // check the access } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Tham số phản hồi: | Tham số | Mô tả | | --------- | ------------------------------------------------------------ | | Profile |

Một đối tượng [AdaptyProfile](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyProfile-class.html). Thông thường, bạn chỉ cần kiểm tra trạng thái mức độ truy cập của hồ sơ người dùng để 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ừ máy chủ, dữ liệu từ bộ nhớ cache sẽ được trả về. Cũng cần lưu ý rằng Adapty SDK thường xuyên cập nhật cache của `AdaptyProfile` để giữ cho thông tin này luôn mới nhất có thể.

| Phương thức `.getProfile()` cung cấp cho bạn hồ sơ người dùng, từ đó bạn có thể kiểm tra trạng thái mức độ truy cập. Mỗi ứng dụng có thể có nhiều mức độ truy cập. Ví dụ, nếu bạn có ứng dụng tin tức và bán gói đăng ký cho các chủ đề khác nhau một cách độc lập, bạn có thể tạo các mức độ truy cập "sports" và "science". Tuy nhiên, trong hầu hết trường hợp, bạn chỉ cần một mức độ truy cập — khi đó bạn có thể dùng mức độ truy cập mặc định "premium". Dưới đây là ví dụ kiểm tra mức độ truy cập mặc định "premium": ```dart showLineNumbers try { final profile = await Adapty().getProfile(); if (profile?.accessLevels['premium']?.isActive ?? false) { // grant access to premium features } } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ### Lắng nghe cập nhật trạng thá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: ```dart showLineNumbers Adapty().didUpdateProfileStream.listen((profile) { // 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. ### Bộ nhớ đệm trạng thái gói đăng ký \{#subscription-status-cache\} Bộ nhớ đệm đượ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 máy chủ không khả dụng, dữ liệu đã được lưu trong bộ nhớ đệm vẫn có thể được truy cập để cung cấp thông tin về trạng thái gói đăng ký của hồ sơ người dùng. Tuy nhiên, cần lưu ý rằng không thể truy vấn dữ liệu trực tiếp từ bộ nhớ cache. SDK sẽ định kỳ truy vấn máy chủ mỗi phút một lần để kiểm tra các cập nhật hoặc thay đổi liên quan đến hồ sơ người dùng. Nếu có bất kỳ thay đổi nào, chẳng hạn như giao dịch mới hoặc các cập nhật khác, chúng sẽ được gửi đến dữ liệu trong bộ nhớ cache để đảm bảo đồng bộ với máy chủ. --- # File: flutter-deal-with-att --- --- title: "Xử lý ATT trong Flutter SDK" description: "Bắt đầu với Adapty trên Flutter để đơ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 ủy 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. ```dart showLineNumbers final builder = AdaptyProfileParametersBuilder() ..setAppTrackingTransparencyStatus(AdaptyIOSAppTrackingTransparencyStatus.authorized); try { await Adapty().updateProfile(builder.build()); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle unknown 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 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-flutter --- --- title: "Chế độ Trẻ em trong Flutter SDK" description: "Dễ dàng bật Chế độ Trẻ em để tuân thủ chính sách của Apple và Google. Không thu thập IDFA, GAID hay dữ liệu quảng cáo trong Flutter SDK." --- Nếu ứng dụng Flutter của bạn dành cho trẻ em, bạn phải tuân thủ chính sách của [Apple](https://developer.apple.com/kids/) và [Google](https://support.google.com/googleplay/android-developer/answer/9893335). 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 đáp ứng các chính sách này và vượt qua quá trình xét duyệt của cửa hàng ứng dụng. ## Yêu cầu là gì? \{#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) (iOS) - [Android Advertising ID (AAID/GAID)](https://support.google.com/googleplay/android-developer/answer/6048248) (Android) - [Đị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 bạn nên thận trọng khi sử dụng customer user ID. User ID có định dạng `` chắc chắn sẽ bị coi là thu thập dữ liệu cá nhân, tương tự như việc sử dụng email. Đối với Kids Mode, phương pháp 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ủ quy định. ## Bật Chế độ Trẻ em \{#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 mã nguồn ứng dụng di động của bạn \{#updates-in-your-mobile-app-code\} Để tuân thủ chính sách, hãy tắt tính năng thu thập IDFA (cho iOS), GAID/AAID (cho Android) và địa chỉ IP của người dùng. **Android: Cập nhật cấu hình SDK của bạn** ```dart showLineNumbers title="Dart" try { await Adapty().activate( configuration: AdaptyConfiguration(apiKey: 'YOUR_API_KEY') // highlight-start ..withGoogleAdvertisingIdCollectionDisabled(true) // set to `true` ..withIpAddressCollectionDisabled(true), // set to `true` // highlight-end ); } catch (e) { // handle the error } ``` **iOS: Bật Chế độ trẻ em trong SDK v4** :::important Trong SDK v4, native iOS SDK được cài đặt qua Swift Package Manager, và Chế độ trẻ em được bật thông qua trait `KidsMode` của Swift package — trait này sẽ loại bỏ toàn bộ code liên quan đến IDFA, AdSupport và AppTrackingTransparency trong quá trình biên dịch. Yêu cầu **Xcode 26** trở lên. ::: Trong SDK v4, hãy dùng package `adapty_flutter_kids` thay cho `adapty_flutter` trong `pubspec.yaml`. Đây là biến thể Chế độ trẻ em của plugin với cùng public API và cùng phiên bản — điểm khác biệt duy nhất là native iOS SDK của nó được build với trait `KidsMode`: ```yaml showLineNumbers title="pubspec.yaml" dependencies: adapty_flutter_kids: 4.0.0 ``` Mã Dart của bạn vẫn giữ nguyên — chỉ cần cập nhật import sang tên package mới: ```dart showLineNumbers title="Dart" ``` **iOS: Bật Kids Mode bằng CocoaPods (SDK v3)** 1. Cập nhật Podfile của bạn: - Nếu bạn **chưa** có mục `post_install`, hãy thêm toàn bộ đoạn code bên dưới. - Nếu bạn **đã** có mục `post_install`, hãy gộp các dòng được highlight 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. Áp dụng các thay đổi bằng cách chạy ```sh showLineNumbers title="Shell" pod install ``` --- # File: flutter-get-onboardings --- --- title: "Lấy onboarding trong Flutter SDK" description: "Tìm hiểu cách lấy onboarding trong Adapty cho Flutter." --- :::warning **Onboardings đã bị deprecated trong SDK v4 và sẽ bị xóa trong phiên bản tương lai.** Chúng không còn nhận được các bản sửa lỗi hay cải tiến nữa. Hãy sử dụng [flows](flutter-get-pb-paywalls) thay thế: khác với onboardings chạy bên trong WebView, flows render trực tiếp trên thiết bị — mang lại hoạt ảnh mượt mà hơn, giao diện native nhất quán, thời gian tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flows & paywalls](flutter-get-pb-paywalls) và [Hiển thị flows & paywalls](flutter-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 Flutter 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 và cấu hình view 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 Flutter SDK](sdk-installation-flutter) 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 dữ liệu onboarding \{#fetch-onboarding\} Khi bạn tạo một [onboarding](onboardings) bằng trình tạo no-code của chúng tôi, nó được lưu trữ dưới dạng một container chứa cấu hình mà ứng dụng cần lấy về và hiển thị. Container này quản lý toàn bộ trải nghiệm — nội dung hiển thị, 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 từ form). Container cũng tự động theo dõi các sự kiện analytics, vì vậy bạn không cần phải tự triển khai việc theo dõi lượt xem riêng. Để có hiệu suất tốt nhất, hãy lấy cấu hình onboarding sớm để hình ảnh có đủ thời gian tải xuống trước khi hiển thị cho người dùng. Để lấy onboarding, hãy dùng phương thức `getOnboarding`: ```dart showLineNumbers try { final onboarding = await Adapty().getOnboarding(placementId: "YOUR_PLACEMENT_ID"); } on AdaptyError catch (e) { //handle error } catch (e) { //handle error } ``` Sau đó, gọi phương thức `createOnboardingView` để lấy view bạn sẽ hiển thị. :::warning Kết quả của phương thức `createOnboardingView` chỉ có thể được sử dụng một lần. Nếu bạn cần sử dụng lại, hãy gọi lại phương thức `createOnboardingView`. Việc gọi hai lần mà không tạo lại có thể dẫn đến lỗi `AdaptyUIError.viewAlreadyPresented`. ::: ```dart showLineNumbers try { final onboardingView = await Adapty().createOnboardingView(onboarding: onboarding); } on AdaptyError catch (e) { //handle error } catch (e) { //handle 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 bản địa hóa của onboarding. Tham số này được kỳ vọng là một mã ngôn ngữ gồm một hoặc hai 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` nghĩa là tiếng Anh, `pt-br` đại diện cho tiếng Bồ Đào Nha Brazil.

| | **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 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 vấn đề với 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 dù kết nối internet 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òn 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 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 để tải onboarding nhanh hơn và một máy chủ dự phòng độc lập phòng 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 onboarding mới nhất trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet bị 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ị đã 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.

| ## Các tham số phản hồi \{#response-parameters\} | Tham số | Mô tả | |:----------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------| | Onboarding | Một đối tượng [`AdaptyOnboarding`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyOnboarding-class.html) chứa: mã định danh và cấu hình onboarding, Remote Config, cùng một số thuộc tính khác. | ## Tăng tốc tải onboarding với onboarding đối tượng mặc định \{#speed-up-onboarding-fetching-with-default-audience-onboarding\} Thông thường, onboarding đượ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à onboarding, và người dùng có kết nối internet yếu, việc tải onboarding 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ị 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ị gì cả. Để giải quyết vấn đề này, bạn có thể dùng phương thức `getOnboardingForDefaultAudience`, phương thức này sẽ 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ư đã mô tả chi tiết trong phần [Fetch Onboarding](#fetch-onboarding) ở trên. :::warning Hãy cân nhắc 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 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 đúng. - **Không có cá nhân hóa**: Chỉ hiển thị nội dung cho đối tượng "Tất cả người dùng", 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 độ tải nhanh hơn quan trọng hơn các hạn chế này trong trường hợp của bạn, hãy sử dụng `getOnboardingForDefaultAudience` như hướng dẫn bên dưới. Nếu không, hãy dùng `getOnboarding` như đã mô tả [ở trên](#fetch-onboarding). ::: ```dart showLineNumbers try { final onboarding = await Adapty().getOnboardingForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID'); } on AdaptyError catch (adaptyError) { // handle error } catch (e) { // handle unknown 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 địa hóa 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 ký tự dấu trừ (**-**). Subtag đầu tiên là ngôn ngữ, subtag thứ hai là khu vực.

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

| | **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 hay gặp tình trạng mạng 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 sẽ có thời gian tải nhanh hơn, bất kể kết nối internet của họ có bập bõm đến đâu. Cache được cập nhật thường xuyên nê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 khi khởi động lại ứng dụng và chỉ bị xóa khi ứng dụng được cài lại hoặc khi 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à các onboarding dự phòng. Chúng tôi cũng sử dụng CDN để tải onboarding nhanh hơn và một máy chủ dự phòng độc lập phòng khi CDN không thể truy cập được. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản onboarding mới nhất trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet hạn chế.

| --- # File: flutter-present-onboardings --- --- title: "Hiển thị onboarding trong Flutter SDK" description: "Tìm hiểu cách hiển thị onboarding hiệu quả để tăng tỷ lệ chuyển đổi." --- :::warning **Onboarding đã bị deprecated trong SDK v4 và sẽ bị xóa trong phiên bản tương lai.** Chúng không còn nhận được các bản sửa lỗi hay cải tiến nữa. Hãy sử dụng [flows](flutter-get-pb-paywalls) thay thế: không giống như onboarding chạy bên trong WebView, flows render trực tiếp trên thiết bị — mang lại hiệu ứng animation mượt mà hơn, giao diện native nhất quán, thời gian tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flows & paywalls](flutter-get-pb-paywalls) và [Hiển thị flows & paywalls](flutter-present-paywalls) để bắt đầu. ::: Nếu bạn đã tùy chỉnh một onboarding bằng builder, bạn không cần lo về việc render nó trong code Flutter để hiển thị cho người dùng. Onboarding đó đã chứa đầy đủ cả nội dung 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 Flutter SDK](sdk-installation-flutter) 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). Adapty Flutter SDK cung cấp hai cách để hiển thị onboarding: - **Màn hình độc lập** - **Widget nhúng** ## Hiển thị dưới dạng màn hình độc lập \{#present-as-standalone-screen\} Để hiển thị một onboarding dưới dạng màn hình độc lập, sử dụng phương thức `onboardingView.present()` trên `onboardingView` được tạo bởi phương thức `createOnboardingView`. Mỗi `view` chỉ có thể được sử dụng một lần. Nếu bạn cần hiển thị onboarding lại, hãy gọi `createOnboardingView` thêm một lần nữa để tạo một instance `onboardingView` mới. :::warning Việc tái sử dụng cùng một `onboardingView` mà không tạo lại có thể dẫn đến lỗi `AdaptyUIError.viewAlreadyPresented`. ::: ```dart showLineNumbers title="Flutter" try { await onboardingView.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ### Đóng onboarding \{#dismiss-the-onboarding\} Khi bạn cần đóng onboarding theo cách lập trình, hãy sử dụng phương thức `dismiss()`: ```dart showLineNumbers title="Flutter" try { await onboardingView.dismiss(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ### Cấu hình kiểu hiển thị trên iOS \{#configure-ios-presentation-style\} Cấu hình cách onboarding được hiển thị trên iOS bằng cách truyền tham số `iosPresentationStyle` vào phương thức `present()`. Tham số này chấp nhận giá trị `AdaptyUIIOSPresentationStyle.fullScreen` (mặc định) hoặc `AdaptyUIIOSPresentationStyle.pageSheet`. ```dart showLineNumbers try { await onboardingView.present(iosPresentationStyle: AdaptyUIIOSPresentationStyle.pageSheet); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ## Nhúng vào cây widget \{#embed-in-widget-hierarchy\} Để nhúng một onboarding vào cây widget hiện có của bạn, hãy sử dụng widget `AdaptyUIOnboardingPlatformView` trực tiếp trong cây widget Flutter của bạn. ```dart showLineNumbers title="Flutter" AdaptyUIOnboardingPlatformView( onboarding: onboarding, // The onboarding object you fetched onDidFinishLoading: (meta) { }, onDidFailWithError: (error) { }, onCloseAction: (meta, actionId) { }, onPaywallAction: (meta, actionId) { }, onCustomAction: (meta, actionId) { }, onStateUpdatedAction: (meta, elementId, params) { }, onAnalyticsEvent: (meta, event) { }, ) ``` :::note Để platform view trên Android hoạt động, đảm bảo `MainActivity` của bạn extends `FlutterFragmentActivity`: ```kotlin showLineNumbers title="Kotlin" class MainActivity : FlutterFragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) } } ``` ::: ## Màn hình tải trong quá trình onboarding \{#loader-during-onboarding\} Khi hiển thị onboarding, bạn có thể thấy một màn hình tải ngắn giữa màn hình splash và onboarding trong khi view bên dưới đang được khởi tạo. Bạn có thể xử lý điều này theo nhiều cách khác nhau tùy theo nhu cầu. #### Kiểm soát màn hình splash bằng onDidFinishLoading \{#control-splash-screen-using-ondidfinishloading\} :::note Cách này chỉ khả dụng khi nhúng onboarding dưới dạng widget. Không áp dụng cho việc trình bày dưới dạng màn hình độc lập. ::: Cách được khuyến nghị cho đa nền tảng là giữ màn hình splash hoặc lớp phủ tùy chỉnh hiển thị cho đến khi onboarding tải xong, rồi ẩn nó đi theo cách thủ công. Khi dùng embedded widget, đặt widget của bạn phủ lên trên và ẩn lớp phủ đó khi `onDidFinishLoading` được kích hoạt: ```dart showLineNumbers title="Flutter" AdaptyUIOnboardingPlatformView( onboarding: onboarding, onDidFinishLoading: (meta) { // Hide your custom splash screen or overlay here }, // ... other callbacks ) ``` ### Tùy chỉnh loader mặc định \{#customize-native-loader\} :::important Cách này phụ thuộc vào từng nền tảng và yêu cầu duy trì code UI native. Không được khuyến nghị trừ khi bạn đã duy trì các native layer riêng biệt trong ứng dụng của mình. ::: Nếu bạn cần tùy chỉnh loader mặc định, bạn có thể thay thế nó bằng các layout theo từng nền tảng. Cách này yêu cầu triển khai riêng cho Android và iOS: - **iOS**: Thêm `AdaptyOnboardingPlaceholderView.xib` vào dự án Xcode của bạn - **Android**: Tạo `adapty_onboarding_placeholder_view.xml` trong `res/layout` và định nghĩa một placeholder ở đó ## Tùy chỉnh cách mở liên kết trong onboarding \{#customize-how-links-open-in-onboardings\} :::important Tính năng tùy chỉnh cách mở liên kết trong onboarding được hỗ trợ từ Adapty SDK v3.15.1 trở lên. ::: Theo mặc định, các liên kết 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 cho người dùng bằng cách hiển thị trang web ngay trong ứng dụng, không cần chuyển sang ứng dụng khác. Nếu bạn muốn mở liên kết trong trình duyệt ngoài, có thể tùy chỉnh hành vi này bằng cách đặt tham số `externalUrlsPresentation` thành `AdaptyWebPresentation.externalBrowser`: ```dart showLineNumbers title="Flutter" final onboardingView = await AdaptyUI().createOnboardingView( onboarding: onboarding, externalUrlsPresentation: AdaptyWebPresentation.externalBrowser, // default – AdaptyWebPresentation.inAppBrowser ); try { await onboardingView.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ```dart showLineNumbers title="Flutter" AdaptyUIOnboardingPlatformView( onboarding: onboarding, externalUrlsPresentation: AdaptyWebPresentation.externalBrowser, // default – AdaptyWebPresentation.inAppBrowser onDidFinishLoading: (meta) { }, onDidFailWithError: (error) { }, onCloseAction: (meta, actionId) { }, onPaywallAction: (meta, actionId) { }, onCustomAction: (meta, actionId) { }, onStateUpdatedAction: (meta, elementId, params) { }, onAnalyticsEvent: (meta, event) { }, ) ``` ## Tắt safe area paddings (Android) \{#disable-safe-area-paddings-android\} Theo mặc định, trên các thiết bị Android, màn hình onboarding sẽ tự động áp dụng safe area paddings để tránh các thành phần giao diện hệ thống như thanh trạng thái và thanh điều hướng. Tuy nhiên, nếu bạn muốn tắt hành vi này và kiểm soát hoàn toàn bố cục, bạn có thể thực hiện bằng cách thêm một boolean resource vào ứng dụng: 1. Truy cập vào `android/app/src/main/res/values`. Nếu chưa có file `bools.xml`, hãy tạo mới. 2. Thêm resource sau: ```xml false ``` Lưu ý rằng các thay đổi này áp dụng toàn cục cho tất cả các onboarding trong ứng dụng của bạn. --- # File: flutter-handling-onboarding-events --- --- title: "Xử lý sự kiện onboarding trong Flutter SDK" description: "Xử lý các sự kiện liên quan đến onboarding trong Flutter bằng Adapty." --- :::warning **Onboardings đã bị deprecated trong SDK v4 và sẽ bị xóa trong các phiên bản tương lai.** Chúng không còn nhận được các bản sửa lỗi hay cải tiến nữa. Hãy sử dụng [flows](flutter-get-pb-paywalls) thay thế: khác với onboardings chạy bên trong WebView, flows render trực tiếp trên thiết bị — mang lại animation mượt mà hơn, giao diện native nhất quán, thời gian tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flows & paywalls](flutter-get-pb-paywalls) và [Hiển thị flows & paywalls](flutter-present-paywalls) để bắt đầu. ::: 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ể phản hồi. Cách xử lý các sự kiện này phụ thuộc vào phương thức hiển thị bạn đang dùng: - **Hiển thị toàn màn hình**: Yêu cầu thiết lập một observer sự kiện toàn cục để xử lý các sự kiện cho tất cả các onboarding view - **Widget nhúng**: Xử lý sự kiện thông qua các tham số callback trực tiếp trong widget Trước khi bắt đầu, hãy đảm bảo rằng: 1. Bạn đã cài đặt [Adapty Flutter SDK](sdk-installation-flutter) 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). ## Sự kiện hiển thị toàn màn hình \{#full-screen-presentation-events\} ### Thiết lập event observer \{#set-up-event-observer\} Để xử lý sự kiện cho các onboarding toàn màn hình, hãy triển khai `AdaptyUIOnboardingsEventsObserver` và thiết lập nó trước khi hiển thị: ```dart showLineNumbers title="Flutter" AdaptyUI().setOnboardingsEventsObserver(this); try { await onboardingView.present(); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ### Xử lý sự kiện \{#handle-events\} Triển khai các phương thức sau trong observer của bạn: ```dart showLineNumbers title="Flutter" void onboardingViewDidFinishLoading( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, ) { // Onboarding finished loading } void onboardingViewDidFailWithError( AdaptyUIOnboardingView view, AdaptyError error, ) { // Handle loading errors } void onboardingViewOnCloseAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { // Handle close action view.dismiss(); } void onboardingViewOnPaywallAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { // Dismiss onboarding before presenting paywall view.dismiss().then((_) { _openPaywall(actionId); }); } void onboardingViewOnCustomAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { // Handle custom actions } void onboardingViewOnStateUpdatedAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String elementId, AdaptyOnboardingsStateUpdatedParams params, ) { // Handle user input updates } void onboardingViewOnAnalyticsEvent( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, AdaptyOnboardingsAnalyticsEvent event, ) { // Track analytics events } ``` ## Sự kiện của widget nhúng \{#embedded-widget-events\} Khi sử dụng `AdaptyUIOnboardingPlatformView`, bạn có thể xử lý các sự kiện thông qua các tham số callback nội tuyến trực tiếp trong widget. Lưu ý rằng các sự kiện sẽ được gửi đến cả callback của widget lẫn observer toàn cục (nếu đã được thiết lập), nhưng observer toàn cục là tùy chọn: ```dart showLineNumbers title="Flutter" AdaptyUIOnboardingPlatformView( onboarding: onboarding, onDidFinishLoading: (meta) { // Onboarding finished loading }, onDidFailWithError: (error) { // Handle loading errors }, onCloseAction: (meta, actionId) { // Handle close action }, onPaywallAction: (meta, actionId) { _openPaywall(actionId); }, onCustomAction: (meta, actionId) { // Handle custom actions }, onStateUpdatedAction: (meta, elementId, params) { // Handle user input updates }, onAnalyticsEvent: (meta, event) { // Track analytics events }, ) ``` ## Các loại sự kiện \{#event-types\} Các phần sau mô tả các loại sự kiện khác nhau mà bạn có thể xử lý, bất kể phương thức hiển thị nào bạn đang sử dụng. ### Xử lý các hành động tùy chỉnh \{#handle-custom-actions\} Trong builder, bạn có thể thêm hành động **custom** vào 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 custom action. Ví dụ: nếu người dùng nhấn vào một nút tùy chỉnh, chẳng hạn như **Login** hoặc **Allow notifications**, delegate method `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 theo ý muốn, chẳng hạn như "allowNotifications". ```dart // Full-screen presentation void onboardingViewOnCustomAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { switch (actionId) { case 'login': _login(); break; case 'allow_notifications': _allowNotifications(); break; } } // Embedded widget onCustomAction: (meta, actionId) { _handleCustomAction(actionId); } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```json { "actionId": "allowNotifications", "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "screensTotal": 3 } } ```
### Hoàn tất tải onboarding \{#finishing-loading-onboarding\} Khi một onboarding hoàn tất tải, sự kiện này sẽ được kích hoạt: ```dart showLineNumbers title="Flutter" // Full-screen presentation void onboardingViewDidFinishLoading( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, ) { print('Onboarding loaded: ${meta.onboardingId}'); } // Embedded widget onDidFinishLoading: (meta) { print('Onboarding loaded: ${meta.onboardingId}'); } ```
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 } } ```
### Đó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ị chính onboarding đó. ::: ```dart showLineNumbers title="Flutter" // Full-screen presentation void onboardingViewOnCloseAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { await view.dismiss(); } // Embedded widget onCloseAction: (meta, actionId) { Navigator.of(context).pop(); } ```
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ở một paywall \{#opening-a-paywall\} :::tip Xử lý sự kiện này để mở một paywall nếu bạn muốn mở nó bên trong onboarding. Nếu bạn muốn mở paywall sau khi nó được đóng lại, có một cách đơn giản hơn — xử lý hành động đóng 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: Lưu ý rằng, trên iOS, chỉ có thể hiển thị một màn hình (paywall hoặc onboarding) tại một thời điểm. Nếu bạn hiển thị một paywall chồng lên một onboarding, bạn không thể điều khiển onboarding ở nền theo cách lập trình. Việc cố gắng đóng 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 đóng màn hình onboarding trước khi hiển thị paywall. ```dart showLineNumbers title="Flutter" // Full-screen presentation void onboardingViewOnPaywallAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String actionId, ) { // Dismiss onboarding before presenting paywall view.dismiss().then((_) { _openPaywall(actionId); }); } Future _openPaywall(String actionId) async { // Implement your paywall opening logic here } // Embedded widget onPaywallAction: (meta, actionId) { _openPaywall(actionId); } ```
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 } } ```
### Theo dõi điều hướng \{#tracking-navigation\} Bạn nhận được một sự kiện analytics khi các sự kiện liên quan đến điều hướng xảy ra trong flow onboarding: ```dart showLineNumbers title="Flutter" // Full-screen presentation void onboardingViewOnAnalyticsEvent( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, AdaptyOnboardingsAnalyticsEvent event, ) { trackEvent(event.type, meta.onboardingId); } // Embedded widget onAnalyticsEvent: (meta, event) { trackEvent(event.type, meta.onboardingId); } ``` Đối tượng `event` có thể là một trong các kiểu sau: | Loại | Mô tả | |------------|-------------| | `onboardingStarted` | Khi onboarding đã được tải | | `screenPresented` | Khi bất kỳ màn hình nào được hiển thị | | `screenCompleted` | Khi một màn hình được hoàn thành. 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 được nhận dạng nào. Bao gồm `name` (tên của sự kiện không xác định) và `meta` (siêu dữ liệu 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 flow onboarding | | `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 |
Ví dụ sự kiện (Nhấp để 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: flutter-onboarding-input --- --- title: "Xử lý dữ liệu từ onboarding trong Flutter SDK" description: "Lưu và sử dụng dữ liệu từ onboarding trong ứng dụng Flutter của bạn với Adapty SDK." --- :::warning **Onboardings đã bị deprecated trong SDK v4 và sẽ bị xóa trong phiên bản tương lai.** Chúng không còn nhận được các bản sửa lỗi hay cải tiến nữa. Hãy sử dụng [flows](flutter-get-pb-paywalls) thay thế: không giống như onboardings chạy trong WebView, flows render trực tiếp trên thiết bị — mang lại hoạt ảnh mượt mà hơn, giao diện native nhất quán, thời gian tải nhanh hơn và không phụ thuộc vào WebView runtime. Xem [Lấy flows & paywalls](flutter-get-pb-paywalls) và [Hiển thị flows & paywalls](flutter-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 input, 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ụ: ```dart // Full-screen presentation void onboardingViewOnStateUpdatedAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String elementId, AdaptyOnboardingsStateUpdatedParams params, ) { // Process data } // Embedded widget onStateUpdatedAction: (meta, elementId, params) { // Process data } ``` Xem định dạng action [tại đây](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyUIOnboardingPlatformView/onStateUpdatedAction.html).
Hình dạng thuộc tính cho từng kiểu params (nhấn để mở rộng) ```dart void onboardingViewOnStateUpdatedAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String elementId, AdaptyOnboardingsStateUpdatedParams params, ) { // elementId là một String: elementId; // 'preference_selector' // meta — AdaptyUIOnboardingMeta: meta.onboardingId; // 'onboarding_123' meta.screenClientId; // 'preferences_screen' meta.screenIndex; // 1 meta.screensTotal; // 3 // params là một trong các lớp con của AdaptyOnboardingsStateUpdatedParams: switch (params) { case AdaptyOnboardingsSelectParams(:final id, :final value, :final label): // một tùy chọn được chọn id; // 'option_1' value; // 'premium' label; // 'Premium Plan' break; case AdaptyOnboardingsMultiSelectParams(:final params): // danh sách các tùy chọn được chọn, mỗi tùy chọn là AdaptyOnboardingsSelectParams params; // [(id: 'interest_1', value: 'sports', label: 'Sports'), (id: 'interest_2', value: 'music', label: 'Music')] break; case AdaptyOnboardingsInputParams(:final input): switch (input) { case AdaptyOnboardingsTextInput(:final value): value; // 'John Doe' break; case AdaptyOnboardingsEmailInput(:final value): value; // 'user@example.com' break; case AdaptyOnboardingsNumberInput(:final value): value; // 25.0 (a double) break; } break; case AdaptyOnboardingsDatePickerParams(:final day, :final month, :final year): day; // 15 month; // 6 year; // 1990 break; } } ```
## Các trường hợp sử dụng \{#use-cases\} ### Làm phong phú hồ sơ người dùng với dữ liệu \{#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ọ hai lần về cùng một thông tin, bạn cần [cập nhật hồ sơ người dùng](flutter-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 của họ vào trường văn bản có ID `name`, và bạn 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 cũng yêu cầu họ nhập email vào trường `email`. Trong code của ứng dụng, nó có thể trông như thế này: ```dart showLineNumbers // Full-screen presentation void onboardingViewOnStateUpdatedAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String elementId, AdaptyOnboardingsStateUpdatedParams params, ) { // Store user preferences or responses if (params is AdaptyOnboardingsInputParams) { final builder = AdaptyProfileParametersBuilder(); // Map elementId to appropriate profile field switch (elementId) { case 'name': if (params.input is AdaptyOnboardingsTextInput) { builder.setFirstName((params.input as AdaptyOnboardingsTextInput).value); } break; case 'email': if (params.input is AdaptyOnboardingsEmailInput) { builder.setEmail((params.input as AdaptyOnboardingsEmailInput).value); } break; } // Update profile Adapty().updateProfile(builder.build()).catchError((error) { // handle the error }); } } // Embedded widget onStateUpdatedAction: (meta, elementId, params) { // Store user preferences or responses if (params is AdaptyOnboardingsInputParams) { final builder = AdaptyProfileParametersBuilder(); // Map elementId to appropriate profile field switch (elementId) { case 'name': if (params.input is AdaptyOnboardingsTextInput) { builder.setFirstName((params.input as AdaptyOnboardingsTextInput).value); } break; case 'email': if (params.input is AdaptyOnboardingsEmailInput) { builder.setEmail((params.input as AdaptyOnboardingsEmailInput).value); } break; } // Update profile Adapty().updateProfile(builder.build()).catchError((error) { // handle the error }); } } ``` ### 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ó thể tùy chỉnh 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 tập thể thao của họ và hiển thị các CTA và 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 ID có ý nghĩa cho các lựa chọn của nó. 2. Xử lý các phản hồi quiz dựa trên ID của chúng và [thiết lập thuộc tính tùy chỉnh](flutter-setting-user-attributes) cho người dùng. ```dart showLineNumbers // Full-screen presentation void onboardingViewOnStateUpdatedAction( AdaptyUIOnboardingView view, AdaptyUIOnboardingMeta meta, String elementId, AdaptyOnboardingsStateUpdatedParams params, ) { // Handle quiz responses and set custom attributes if (params is AdaptyOnboardingsSelectParams) { final builder = AdaptyProfileParametersBuilder(); // Map quiz responses to custom attributes switch (elementId) { case 'experience': // Set custom attribute 'experience' with the selected value (beginner, amateur, pro) builder.setCustomStringAttribute(params.value, 'experience'); break; } // Update profile Adapty().updateProfile(builder.build()).catchError((error) { // handle the error }); } } // Embedded widget onStateUpdatedAction: (meta, elementId, params) { // Handle quiz responses and set custom attributes if (params is AdaptyOnboardingsSelectParams) { final builder = AdaptyProfileParametersBuilder(); // Map quiz responses to custom attributes switch (elementId) { case 'experience': // Set custom attribute 'experience' with the selected value (beginner, amateur, pro) builder.setCustomStringAttribute(params.value, 'experience'); break; } // Update profile Adapty().updateProfile(builder.build()).catchError((error) { // handle the error }); } } ``` 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](flutter-paywalls) cho placement trong code của ứng dụng. Nếu onboarding của bạn có nút mở paywall, hãy triển khai code paywall như một [phản hồi cho hành động của nút này](flutter-handling-onboarding-events#opening-a-paywall). --- # File: flutter-sdk-call-order --- --- title: "Thứ tự gọi trong Flutter 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 không 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`](error-handling-on-flutter-react-native-unity#custom-network-codes). 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 hành động người dùng cho đến khi `identify` hoàn tất. Các lệnh gọi chạy song song với nó có thể thất bại với [`#3006 profileWasChanged`](error-handling-on-flutter-react-native-unity#custom-network-codes), hoặc sẽ tác động lên hồ sơ người dùng ẩn danh được tạo lúc kích hoạt. 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. SDK của MMP và analytics (AppsFlyer, Adjust, Branch, PostHog) tuân theo quy tắc tương tự. 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 hồ sơ người dùng ẩn danh tạm thời và không phải lúc nào cũng được chuyển sang hồ sơ đã xác định. Để biết thêm chi tiết về AppsFlyer, xem [AppsFlyer](appsflyer). ## Thứ tự đúng \{#the-correct-order\} Lộ trình của bạn phụ thuộc vào hai điều: thời điểm bạn biết được customer user ID, và việc bạn có sử dụng MMP hoặc analytics SDK hay không. - **Bước 2 và 5**: Bắt buộc với mọi ứng dụng. Kích hoạt SDK, sau đó gọi các phương thức SDK. - **Bước 1 và 3**: Chỉ cần thiết nếu bạn tích hợp MMP hoặc analytics SDK (AppsFlyer, Adjust, Branch, PostHog). - **Bước 4**: Chỉ cần thiết 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 động. Nếu bạn đã có customer user ID ngay khi ứng dụng khởi động, hãy truyền trực tiếp vào `activate()` (bước 2a). Cách này không bao giờ tạo hồ sơ người dùng ẩn danh, nên bước 4 là không cần thiết. | Bước | Lệnh gọi | Thời điểm | Ghi chú | |------|---------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------| | 1 | Khởi tạo MMP hoặc analytics SDK của bạn (AppsFlyer, Adjust, PostHog, Branch) | Khởi động ứng dụng, đầu tiên | Chờ callback UID của MMP, ví dụ `getAppsFlyerUID`. | | 2a | `Adapty().activate(configuration: ...)` với `withCustomerUserId` được thiết lập trong cấu hình | Khởi động ứng dụng, sau bước 1, nếu bạn đã có customer user ID | Khuyến nghị. Không bao giờ tạo hồ sơ người dùng ẩn danh. | | 2b | `Adapty().activate(configuration: ...)` không có `withCustomerUserId` | Khởi động ứng dụng, sau bước 1, nếu bạn chưa 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(key: ..., value: ...)` cho từng MMP | Sau bước 2, trước bất kỳ lệnh gọi nào do người dùng thực hiện | Bắt buộc để các ID của MMP gắn đúng vào hồ sơ người dùng. | | 4 | `await Adapty().identify(customerUserId)` | Sau bước 3 (hoặc bước 2 nếu không có MMP), trước bước 5 — chỉ trên luồng 2b có xác thực | Luôn dùng `await`. Các lệnh gọi đồng thời trong quá trình `identify` sẽ trả về `#3006 profileWasChanged`. | | 5 | `getPaywall` (`getFlow` trong SDK v4), `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ẽ dẫn đến 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ề cho 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) 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ơ ẩ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 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 đó gọi `restorePurchases`. Về metadata cần gửi kèm mỗi web checkout, xem: - [Stripe](stripe) - [Paddle](paddle) --- # File: flutter-optimize-paywall-fetching --- --- title: "Tối ưu hóa việc tải paywall trong Flutter SDK" description: "Tải paywall Adapty một cách đáng tin cậy: thời điểm, bộ nhớ đệm và các pattern dự phòng cho Flutter." --- Một lần tải paywall đáng tin cậy trên Flutter cần đảm bảo ba điều: hiển thị nhanh, trả về đúng paywall theo đối tượng người dùng, và xử lý dự phòng khi mạng chậm. Các quy tắc dưới đây đề cập đến thời điểm, bộ nhớ đệm và các pattern dự phòng để đạt được điều đó. :::tip Các quy tắc này giả định rằng `Adapty().activate()` và `Adapty().identify()` đã được giải quyết xong. Xem [Thứ tự gọi trong Flutter SDK](flutter-sdk-call-order). ::: Các hướng dẫn dưới đây sử dụng tên phương thức v3. Trong SDK v4, `getPaywall` được đổi tên thành `getFlow` và kiểu fetch policy thành `AdaptyFlowFetchPolicy` — mọi quy tắc đều áp dụng không thay đổi. ## Quy tắc và lưu ý \{#rules-and-pitfalls\} | Nên làm | Không nên làm | Lý do | |---|---|---| | Fetch placement bạn sắp hiển thị. | Prefetch tất cả placement đồng thời khi khởi động. | Bulk prefetch chặn main thread và gây màn hình đen trong thời gian burst. | | Fetch `getPaywall` sau khi attribution đã có thời gian xử lý — ví dụ, 1–2 giây sau `activate` hoặc sau khi `didUpdateProfileStream` kích hoạt. | Gọi `getPaywall` trong `main()` trước `runApp`. | Attribution chưa được xử lý xong. Paywall sẽ được resolve theo đối tượng mặc định và âm thầm bỏ qua các phân khúc cũng như ASA personalization. | | Đặt `loadTimeout` và cấu hình [paywall dự phòng](fallback-paywalls) cho mỗi placement. | Chờ `getPaywall` vô thời hạn. | 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ản hồi — hoặc họ sẽ đóng ứng dụng. | Xem [Tải paywall và sản phẩm](fetch-paywalls-and-products-flutter) để tham khảo thông số `fetchPolicy` và `loadTimeout`, và [Placements](placements) để chọn placement phù hợp. ## 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 liên tục kém (khu vực nông thôn, phương tiện công cộng, vùng bị ảnh hưởng bởi định tuyến mạng): - Đặt `fetchPolicy: AdaptyPaywallFetchPolicy.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 paywall dự phòng khi timeout xảy ra. - Không chặn việc hiển thị paywall bởi `getProfile()`. Gọi `getPaywall` độc lập để một profile phản hồi chậm không làm tắc nghẽn giao diện. --- # File: flutter-show-aa-targeted-paywall --- --- title: "Hiển thị paywall theo mục tiêu AA khi khởi chạy lần đầu trong Flutter SDK" description: "Chờ một chút để nhận attribution từ Apple Ads trước khi hiển thị paywall khi khởi chạy lần đầu trong Flutter, tự động chuyển về đối tượng mặc định khi hết thời gian chờ. Sử dụng AdaptyProfile.appliedAttributionSources." --- Attribution từ Apple Ads (AA) được nhận bất đồng bộ sau khi gọi `Adapty().activate()`. Ở lần khởi chạy đầu tiên, dữ liệu này thường chưa có, vì vậy nếu bạn gọi `getPaywall` ngay lập tức, Adapty sẽ xử lý yêu cầu dựa trên đối tượng mặc định và những người dùng Apple Ads sẽ không thấy paywall được nhắm mục tiêu theo AA. Thay vì hiển thị một paywall rồi thay thế nó, hãy chờ một chút để nhận attribution từ AA trước khi hiển thị bất cứ thứ gì: hiển thị paywall đúng đối tượng nếu attribution đến trong thời gian chờ ngắn, hoặc hiển thị paywall theo đối tượng mặc định nếu không. `AdaptyProfile.appliedAttributionSources` cho bạn biết khi nào attribution từ AA đã được áp dụng. ## Trước khi bắt đầu \{#before-you-start\} Bạn cần: - Adapty Flutter SDK **3.17.0** 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 từ Apple Ads ở chế độ nền và chuyển kết quả đó về 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ẽ gửi một `AdaptyProfile` đã được cập nhật đến listener `didUpdateProfileStream` của bạn, với `AdaptyAttributionSource.appleAds` trong danh sách `appliedAttributionSources`. Ở lần chạy đầu tiên, bạn sẽ cần xử lý hai trường hợp: 1. **Attribution đến trong thời gian chờ của bạn.** Gọi `getPaywall` — Adapty xử lý yêu cầu dựa trên đối tượng Apple Ads và trả về paywall được nhắm mục tiêu. 2. **Thời gian chờ hết trước.** Hiển thị paywall cho đối tượng mặc định thay thế, để người dùng không có attribution Apple Ads không phải chờ đợi. `getPaywallForDefaultAudience` trả về paywall đó mà không cần chờ phân khúc. `appliedAttributionSources` có thể rỗng. Điều đó có nghĩa là: - Attribution Apple Ads chưa được xử lý cho hồ sơ người dùng này, hoặc - không có attribution nào đến cả. Dù thế nào, `getPaywallForDefaultAudience` vẫn an toàn để gọi — nó trả về paywall cho đối tượng mặc định bất kể trạng thái hồ sơ người dùng. :::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 lại, nó sẽ đượ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ơ người dùng đã được cache sẵn `AdaptyAttributionSource.appleAds` trong `appliedAttributionSources`, nên đường dẫn attribution được xử lý ngay lập tức và `getPaywall` trả về paywall được phân khúc theo Apple Ads mà không có bất kỳ độ trễ nào. ::: ## Triển khai \{#implementation\} Khi khởi chạy lần đầu, hãy chờ `AdaptyAttributionSource.appleAds` và áp dụng thời gian chờ tối đa — nếu attribution từ 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 Flutter SDK](sdk-installation-flutter). 2. **Đăng ký nhận cập nhật hồ sơ người dùng** với `Adapty().didUpdateProfileStream.listen(…)`. Nếu bạn chưa thiết lập listener, xem [Lắng nghe cập nhật gói đăng ký](flutter-check-subscription-status#listen-to-subscription-updates). 3. **Theo dõi `AdaptyAttributionSource.appleAds` trong `appliedAttributionSources`.** Khi nó xuất hiện, tải paywall bằng `getPaywall` — Adapty sẽ trả về biến thể được phân khúc theo AA: ```dart final subscription = Adapty().didUpdateProfileStream.listen((profile) async { if (!profile.appliedAttributionSources.contains(AdaptyAttributionSource.appleAds)) return; final paywall = await Adapty().getPaywall(placementId: placementId); // present the segmented paywall, then cancel the subscription and the timer }); ``` `didUpdateProfileStream` là một broadcast stream và không replay, vì vậy hãy kiểm tra hồ sơ người dùng hiện tại một lần bằng `getProfile()`. Khi khởi động lại ứng dụng, attribution đã được lưu trữ sẽ không phát lại sự kiện. 4. **Bắt đầu đồng hồ đếm ngược 3–5 giây song song với gói đăng ký.** Nếu đồng hồ kích hoạt trước khi `AdaptyAttributionSource.appleAds` xuất hiện, hãy tải paywall cho đối tượng mặc định bằng `getPaywallForDefaultAudience`. Hiển thị paywall nào được trả về trước và hủy luồng còn lại để tránh tải paywall hai lần. Hãy cấu hình [paywall dự phòng](flutter-use-fallback-paywalls) cho placement đó để người dùng không bị kẹt nếu yêu cầu mạng thất bại. ## Ví dụ đầy đủ \{#complete-example\} Cách triển khai dưới đây chạy đua giữa attribution và một khoảng timeout, tải trước paywall cho đối tượng mặc định song song, và trả về paywall phù hợp. Phía gọi hàm chỉ cần `await` một hàm duy nhất — không cần listener hay cờ trạng thái nào ở call site: - Nếu attribution về kịp trong `timeout`, hàm trả về paywall theo phân khúc thông qua `getPaywall`. - Nếu `timeout` hết trước, hàm trả về paywall đối tượng mặc định đã tải trước thông qua `getPaywallForDefaultAudience`. ```dart title="apple_ads_paywall.dart" /// Returns the Apple Ads-segmented paywall if attribution is applied within /// [timeout], otherwise the default-audience paywall. Call after Adapty().activate(). Future getPaywallOrDefault({ required String placementId, required Duration timeout, }) { // Prefetch the default-audience paywall right away so the timeout path resolves // without an extra network round-trip. `getPaywallForDefaultAudience` skips the // wait for segmentation data. `..ignore()` keeps an unused prefetch from surfacing // as an unhandled error; the error still reaches the caller if this paywall wins. final defaultPaywall = Adapty().getPaywallForDefaultAudience(placementId: placementId)..ignore(); final completer = Completer(); late final StreamSubscription subscription; late final Timer timer; void resolve(Future paywall) { if (completer.isCompleted) return; timer.cancel(); subscription.cancel(); completer.complete(paywall); } void onProfile(AdaptyProfile profile) { if (profile.appliedAttributionSources.contains(AdaptyAttributionSource.appleAds)) { resolve(Adapty().getPaywall(placementId: placementId)); } } // Attribution path: react to profile updates as attribution is applied. subscription = Adapty().didUpdateProfileStream.listen(onProfile); // The stream is a broadcast stream and doesn't replay, so check the current // profile too — on relaunches attribution is already stored and won't re-emit. Adapty().getProfile().then(onProfile).ignore(); // Timeout path: fall back to the prefetched default-audience paywall. timer = Timer(timeout, () => resolve(defaultPaywall)); return completer.future; } ``` Gọi từ màn hình splash của bạn, sau đó hiển thị paywall khi promise được giải quyết: ```dart try { final paywall = await getPaywallOrDefault( placementId: 'YOUR_PLACEMENT_ID', timeout: const Duration(seconds: 5), ); // present the paywall } on AdaptyError catch (adaptyError) { // handle the error or show a fallback paywall } catch (e) { // handle the error } ``` Điều chỉnh `timeout` theo thời gian bạn muốn người dùng chờ trước khi paywall xuất hiện. Hầu hết người dùng không có attribution từ Apple Ads, nên họ sẽ chờ hết toàn bộ thời gian timeout — 3 đến 5 giây là mức cân bằng hợp lý. Attribution nếu có thường đến trong vài giây sau khi khởi động ứng dụng. Nếu ứng dụng của bạn đã lắng nghe `didUpdateProfileStream` cho các mục đích khác (ví dụ: [kiểm tra trạng thái gói đăng ký](flutter-check-subscription-status#listen-to-subscription-updates)), bạn không cần thay đổi gì. `didUpdateProfileStream` là một broadcast stream, nên nó hỗ trợ nhiều listener độc lập mà không ảnh hưởng đến nhau. --- # File: flutter-test --- --- title: "Test & release in Flutter SDK" description: "Tìm hiểu cách kiểm tra trạng thái gói đăng ký trong ứng dụng Flutter của bạn với Adapty." --- Nếu bạn đã tích hợp Adapty SDK vào ứng dụng Flutter của mình, bước tiếp theo là 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 trên cả hai nền tảng iOS và Android không. Quá trình này bao gồm việc kiểm tra cả tích hợp SDK lẫn luồng mua hàng thực tế với môi trường sandbox của Apple và môi trường thử nghiệm của Google Play. ## Kiểm thử ứng dụng \{#test-your-app\} Để kiểm thử in-app purchase một cách toàn diện, hãy xem các hướng dẫn kiểm thử theo nền tảng: [Hướng dẫn kiểm thử iOS](test-purchases-in-sandbox) và [Hướng dẫn kiểm thử Android](testing-on-android). ## Chuẩn bị phát hành \{#prepare-for-release\} Trước khi nộp ứ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 từ 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à xét duyệt đã được đáp ứng --- # File: InvalidProductIdentifiers-flutter --- --- title: "Sửa lỗi Code-1000 noProductIDsFound trong Flutter SDK" description: "Khắc phục lỗi mã sản phẩm không hợp lệ khi quản lý gói đăng ký trong Adapty." --- Lỗi mã 1000, `noProductIDsFound`, cho biết không có sản phẩm nào bạn yêu cầu trên paywall có thể mua được trong App Store, mặc dù chúng đã được đăng 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\} 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-3-check-products\} 1. Truy cập **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 phần **Subscriptions**. 3. Đảm bảo sản phẩm bạn đang kiểm tra được đánh dấu là **Ready to Submit**. 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 một 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ở phần **Subscriptions** tương tự. 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 phần **Availability** và kiểm tra xem tất cả các quốc gia và khu vực cần thiết đã được liệt kê chưa. ## Bước 4. Kiểm tra giá sản phẩm \{#step-5-check-product-prices\} 1. Tiếp tục vào phần **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 phần **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 đã được liệt kê. ## Bước 5. Kiểm tra trạng thái ứng dụng trả phí, 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 xem **Paid Apps Agreement**, **Bank Account** và **Tax forms** của bạn có đều hiển thị là **Active** không. 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 của mình 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ề `1000 noProductIDsFound`. Trong trường hợp đó, sản phẩm có thể 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 xuất hiện trên đườ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. Chờ tối đa 24 giờ sau khi tạo lại để các thay đổi được lan truyền. --- # File: cantMakePayments-flutter --- --- title: "Sửa lỗi Code-1003 cantMakePayment trong Flutter SDK" 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-flutter-sdk-v4 --- --- title: "Migrate Adapty Flutter SDK sang phiên bản 4.0" description: "Migrate lên Adapty Flutter SDK v4.0 bằng cách thay thế các API paywall bằng API flow, tương thích với cả Flow Builder và Paywall Builder." --- Adapty Flutter SDK 4.0 giới thiệu flow và đổi tên các API paywall tương ứng. Các API mới hoạt động với cả Flow Builder mới và Paywall Builder hiện có — không cần thay đổi cấu hình nào trên Adapty Dashboard. ## Tham khảo nhanh \{#quick-reference\} | v3 | v4 | |---|---| | `Adapty().getPaywall(placementId: id)` | `Adapty().getFlow(placementId: id)` | | `Adapty().getPaywallForDefaultAudience(placementId: id)` | `Adapty().getFlowForDefaultAudience(placementId: id)` | | `Adapty().getPaywallProducts(paywall: paywall)` | `Adapty().getPaywallProducts(flow: flow)` | | `Adapty().logShowPaywall(paywall: paywall)` | `Adapty().logShowFlow(flow: flow)` | | `AdaptyPaywall` (kiểu) | `AdaptyFlow` | | `AdaptyPaywallFetchPolicy` (kiểu) | `AdaptyFlowFetchPolicy` | | `AdaptyUI().createPaywallView(paywall: paywall)` | `AdaptyUI().createFlowView(flow: flow)` | | `AdaptyUIPaywallView` (kiểu) | `AdaptyUIFlowView` | | `AdaptyUIPaywallPlatformView` (widget) | `AdaptyUIFlowPlatformView` | | `AdaptyUI().presentPaywallView(view)` / `dismissPaywallView(view)` | `AdaptyUI().presentFlowView(view)` / `dismissFlowView(view)` | | `AdaptyUIPaywallsEventsObserver` | `AdaptyUIFlowsEventsObserver` | | `AdaptyUI().setPaywallsEventsObserver(observer)` | `AdaptyUI().setFlowsEventsObserver(observer)` | | callback `paywallViewDid*` | callback `flowViewDid*` | | `paywallViewDidFailRendering` | `flowViewDidReceiveError` | `AdaptyPaywallProduct` vẫn giữ nguyên tên — các sản phẩm vẫn thuộc về một flow, và `getPaywallProducts` giờ nhận vào một `AdaptyFlow`. Bạn không còn cần truyền `locale` khi lấy một flow nữa. Các API mua hàng và hồ sơ người dùng (`makePurchase`, `restorePurchases`, `getProfile`, `identify`, v.v.) không thay đổi, tương tự với các phương thức giao diện `present`, `dismiss` và `showDialog`. Một số hành vi mặc định đã thay đổi — xem [Thay đổi hành vi mặc định](#default-behavior-changes). ## Phiên bản tối thiểu \{#minimum-versions\} Adapty Flutter SDK 4.0 nâng cao yêu cầu tối thiểu: - **iOS 15.0** — deployment target iOS tối thiểu, tăng từ iOS 13.0. - **Xcode 26** trở lên — iOS SDK native sử dụng Swift tools 6.2. - **Flutter 3.32.0** (Dart 3.8.0) trở lên. ## Cài đặt \{#installation\} ### Cập nhật package \{#update-the-package\} Package bạn cài đặt phụ thuộc vào việc ứng dụng có sử dụng Kids Mode hay không. Với hầu hết các ứng dụng, hãy cập nhật `adapty_flutter` lên v4.0 trong `pubspec.yaml`: ```yaml showLineNumbers title="pubspec.yaml" dependencies: adapty_flutter: 4.0.0 ``` Nếu ứng dụng sử dụng Kids Mode, hãy chỉ định `adapty_flutter_kids` thay thế: ```yaml showLineNumbers title="pubspec.yaml" dependencies: adapty_flutter_kids: 4.0.0 ``` Gói **độc lập** này loại bỏ mã IDFA và theo dõi quảng cáo để tuân thủ các yêu cầu của App Store. Cập nhật đường dẫn import Dart thành `package:adapty_flutter_kids/adapty_flutter.dart`. Ngoài ra, quá trình migration hoàn toàn giống với gói thông thường. Kids Mode cũng yêu cầu bạn tắt tính năng thu thập địa chỉ IP trong Adapty Dashboard — xem [Kids Mode](kids-mode-flutter) để biết hướng dẫn cài đặt đầy đủ. ### iOS: SDK iOS native giờ được phân phối qua Swift Package Manager \{#ios-native-sdks-now-come-through-swift-package-manager\} [Repo spec của CocoaPods sẽ chuyển sang chế độ chỉ đọc vào tháng 12 năm 2026](https://blog.cocoapods.org/CocoaPods-Specs-Repo/), vì vậy bắt đầu từ v4, SDK iOS native **không còn được phân phối qua CocoaPods** nữa — plugin sẽ kéo nó qua **Swift Package Manager** mà thôi. Nếu bạn đang dùng Flutter 3.32–3.43, hãy bật hỗ trợ Swift Package Manager một lần: ```bash flutter config --enable-swift-package-manager ``` Flutter 3.44 trở lên đã bật Swift Package Manager theo mặc định, nên bạn không cần làm gì thêm. ## Lấy flows \{#fetching-flows\} ### getPaywall → getFlow Kiểu trả về thay đổi từ `AdaptyPaywall` sang `AdaptyFlow`, và bạn không cần truyền `locale` nữa — khi render một flow, localization sẽ được tự động xử lý; với các paywall tùy chỉnh, tất cả locale đã cấu hình sẽ được trả về trong `flow.remoteConfigs`: ```diff showLineNumbers - final paywall = await Adapty().getPaywall(placementId: 'YOUR_PLACEMENT_ID', locale: 'en'); + final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID'); ``` `getPaywallForDefaultAudience` cũng được đổi tên theo cách tương tự: ```diff showLineNumbers - final paywall = await Adapty().getPaywallForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID', locale: 'en'); + final flow = await Adapty().getFlowForDefaultAudience(placementId: 'YOUR_PLACEMENT_ID'); ``` Kiểu fetch policy được đổi tên từ `AdaptyPaywallFetchPolicy` thành `AdaptyFlowFetchPolicy`; các tùy chọn của nó (`reloadRevalidatingCacheData`, `returnCacheDataElseLoad`, `returnCacheDataIfNotExpiredElseLoad`) không thay đổi. ### getPaywallProducts(paywall) → getPaywallProducts(flow) `getPaywallProducts` giữ nguyên tên nhưng giờ nhận một `AdaptyFlow` qua tham số `flow`: ```diff showLineNumbers - final products = await Adapty().getPaywallProducts(paywall: paywall); + final products = await Adapty().getPaywallProducts(flow: flow); ``` ## Mô hình dữ liệu \{#data-model\} `getFlow` trả về `AdaptyFlow` thay vì `AdaptyPaywall`, và cấu trúc đối tượng đã thay đổi: | Thành viên `AdaptyPaywall` v3 | Thành viên `AdaptyFlow` v4 | Hành động | |---|---|---| | `remoteConfig` (đơn, nullable) | `remoteConfigs` (danh sách) | Một flow chứa một remote config cho mỗi ngôn ngữ được cấu hình. Getter `remoteConfig` vẫn tồn tại và trả về mục đầu tiên; để chọn một ngôn ngữ cụ thể, hãy tìm trong `remoteConfigs` theo `locale` của nó. | | `productIdentifiers` | `productIdentifiers` | Được giữ lại, nhưng hiện được thu thập từ tất cả các biến thể paywall trong flow. Các identifier theo từng biến thể nằm ở `flow.paywalls[i].productIdentifiers`. | | `hasViewConfiguration` | `hasViewConfiguration` | Không thay đổi. | | `placementId` (deprecated) | đã xóa | Dùng `flow.placement.id`. | | `revision` (deprecated) | đã xóa | Dùng `flow.placement.revision`. | | `vendorProductIds` (deprecated) | đã xóa | Dùng `productIdentifiers`. | | _(mới)_ | `paywalls` (danh sách `AdaptyFlowPaywall`) | Mỗi mục là một biến thể paywall trong flow, với `name`, `variationId` và `productIdentifiers` riêng. | `AdaptyPaywallViewConfiguration` is no longer exposed — the view configuration is now opaque. Remove any references to this type. ## Phương thức paywall web \{#web-paywall-methods\} `openWebPaywall` và `createWebPaywallUrl` giữ nguyên tên, nhưng tham số `paywall` giờ nhận `AdaptyFlowPaywall` (một biến thể flow) thay vì `AdaptyPaywall`. Bạn vẫn có thể truyền `AdaptyPaywallProduct` như trước. ```diff showLineNumbers final flow = await Adapty().getFlow(placementId: 'YOUR_PLACEMENT_ID'); - await Adapty().openWebPaywall(paywall: paywall); + if (flow.paywalls.isNotEmpty) { + await Adapty().openWebPaywall(paywall: flow.paywalls[0]); + } ``` ## Theo dõi lượt xem flow \{#tracking-flow-views\} ### logShowPaywall → logShowFlow `logShowPaywall` được đổi tên thành `logShowFlow` và nhận vào một `AdaptyFlow`. Sự kiện vẫn được ghi lại đối với 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 gì trên dashboard. ```diff showLineNumbers - await Adapty().logShowPaywall(paywall: paywall); + await Adapty().logShowFlow(flow: flow); ``` Giống như ở v3, bạn không cần gọi phương thức này khi hiển thị các flow hoặc paywall được dựng bởi [Flow Builder](adapty-flow-builder) hay [Paywall Builder](adapty-paywall-builder) — Adapty tự động theo dõi các lượt xem đó. ## Hiển thị flows \{#displaying-flows\} ### createPaywallView → createFlowView Đổi tên phương thức và truyền `AdaptyFlow` qua tham số `flow`. Các tham số khác (`loadTimeout`, `preloadProducts`, `customTags`, `customTimers`, `customAssets`, `productPurchaseParams`) không thay đổi, cũng như các phương thức của view là `present`, `dismiss`, và `showDialog`: ```diff showLineNumbers - final view = await AdaptyUI().createPaywallView(paywall: paywall); + final view = await AdaptyUI().createFlowView(flow: flow); await view.present(); ``` ### AdaptyUIPaywallView → AdaptyUIFlowView Kiểu view được đổi tên. Thuộc tính `paywallVariationId` đã bị xóa — hãy dùng `variationId` thay thế: ```diff showLineNumbers - void flowViewDidAppear(AdaptyUIPaywallView view) { + void flowViewDidAppear(AdaptyUIFlowView view) { ``` ### AdaptyUIPaywallPlatformView → AdaptyUIFlowPlatformView Nếu bạn nhúng view dưới dạng widget trong widget tree, hãy đổi tên nó và truyền tham số `flow`. Các callback sự kiện (`onDidAppear`, `onDidFinishPurchase`, v.v.) vẫn giữ nguyên tên: ```diff showLineNumbers - AdaptyUIPaywallPlatformView( - paywall: paywall, + AdaptyUIFlowPlatformView( + flow: flow, onDidFinishPurchase: (view, product, purchaseResult) { /* … */ }, ) ``` :::note Một flow view được tạo bằng `createFlowView` chỉ dùng được một lần: sau khi bạn gọi `dismiss()`, view đó sẽ được giải phóng khỏi bộ nhớ và không thể hiển thị lại — hãy gọi `createFlowView` một lần nữa nếu muốn hiển thị flow lại. ::: ## Xử lý sự kiện \{#handling-events\} Tên lớp observer được đổi từ `AdaptyUIPaywallsEventsObserver` thành `AdaptyUIFlowsEventsObserver`, phương thức đăng ký từ `setPaywallsEventsObserver` thành `setFlowsEventsObserver`, và tất cả các callback `paywallViewDid*` thành `flowViewDid*`: ```diff showLineNumbers - class MyObserver extends AdaptyUIPaywallsEventsObserver { + class MyObserver extends AdaptyUIFlowsEventsObserver { @override - void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) { + void flowViewDidPerformAction(AdaptyUIFlowView view, AdaptyUIAction action) { // … } } - AdaptyUI().setPaywallsEventsObserver(this); + AdaptyUI().setFlowsEventsObserver(this); ``` Ba callback sau đây là **bắt buộc** — observer của bạn sẽ không biên dịch được nếu thiếu chúng: - **`flowViewDidFinishPurchase`**: Trước đây là tùy chọn trong v3, mặc định sẽ dismiss view sau khi mua hàng. Giờ bạn tự quyết định: tiếp tục flow hay gọi `view.dismiss()`. - **`flowViewDidFinishRestore`**: Bắt buộc, giống như trong v3. - **`flowViewDidReceiveError`**: Thay thế `paywallViewDidFailRendering` và giờ cũng nhận các lỗi view khác. Hai thay đổi nhỏ hơn: - `setFlowsEventsObserver` (và `setOnboardingsEventsObserver`) giờ chấp nhận `null` để gỡ bỏ observer đã đặt trước đó, giúp SDK không còn giữ tham chiếu đến nó nữa. - Callback tùy chọn mới `flowViewDidReceiveAnalyticEvent` được dành riêng cho các sự kiện analytic tùy chỉnh từ một flow. Hiện tại các flow chưa phát ra sự kiện này đến code của bạn, nên bạn không cần implement nó. v4 cũng bổ sung các tính năng bạn có thể chọn dùng: - `AdaptyUI().setObserverModeResolver(...)` với một `AdaptyUIObserverModeResolver` — xử lý các giao dịch mua và khôi phục được khởi tạo từ flow khi SDK chạy ở [chế độ Observer](implement-observer-mode-flutter). Trước đây tính năng này chỉ có trong SDK iOS và Android gốc. Xem [Hiển thị flow trong chế độ Observer](flutter-present-flows-in-observer-mode). - `AdaptyUI().setSystemRequestsHandler(...)` với một `AdaptyUISystemRequestsHandler` — dành riêng cho các yêu cầu hệ thống từ flow (lời nhắc cấp quyền của hệ điều hành và yêu cầu đánh giá App Store). Hiện tại flow chưa kích hoạt các yêu cầu này, nên bạn không cần đăng ký handler. ## Các API đã bị xóa \{#removed-apis\} Các ký hiệu này đã bị deprecated trong 3.x và bị xóa trong v4: ### setFallbackPaywalls → setFallback ```diff showLineNumbers - await Adapty().setFallbackPaywalls(assetId); + await Adapty().setFallback(assetId); ``` ### withIdfaCollectionDisabled → withAppleIdfaCollectionDisabled ```diff showLineNumbers configuration: AdaptyConfiguration(apiKey: 'YOUR_PUBLIC_SDK_KEY') - ..withIdfaCollectionDisabled(true), + ..withAppleIdfaCollectionDisabled(true), ``` ### Các thành phần đã bị xóa khác \{#other-removed-members\} - **`AdaptyPurchaseResultSuccess.jwsTransaction`**: Sử dụng `appleJwsTransaction`. - **`AdaptyUIFlowView.paywallVariationId`**: Sử dụng `variationId`. - **`AdaptyUIObserver` và `AdaptyUI().setObserver(...)`**: Sử dụng `AdaptyUIFlowsEventsObserver` và `setFlowsEventsObserver(...)`. ## Thay đổi hành vi mặc định \{#default-behavior-changes\} Những thay đổi này không gây ra lỗi biên dịch, vì vậy hãy kiểm tra chúng ở runtime: - **Mua hàng thành công**: Trong v3, `paywallViewDidFinishPurchase` mặc định sẽ đóng view. Trong v4, `flowViewDidFinishPurchase` là bắt buộc và không có hành vi mặc định — bạn cần tự đóng view nếu muốn. - **Nút back hệ thống Android**: Nút này không còn đóng flow theo mặc định nữa. Hành động sẽ được gửi đến `flowViewDidPerformAction` dưới dạng `AndroidSystemBackAction` — hãy xử lý ở đó nếu muốn nút back đóng flow. - **Mở URL**: `flowViewDidPerformAction` mặc định hiện xử lý `OpenUrlAction` bằng cách mở URL theo cách native (tôn trọng cài đặt trình duyệt in-app hoặc bên ngoài từ dashboard), ngoài việc đóng view khi nhận `CloseAction`. Ghi đè callback này nếu bạn muốn tự xử lý URL. - **Lỗi view**: `flowViewDidReceiveError` là bắt buộc, và việc đóng view tùy thuộc vào cách bạn triển khai. Nếu tích hợp v3 của bạn phụ thuộc vào việc view tự đóng khi có lỗi render, hãy gọi `view.dismiss()` trong callback này. - **Vòng đời view**: Đóng một flow hoặc onboarding view sẽ giải phóng nó khỏi bộ nhớ. View đã đóng không thể hiển thị lại — hãy tạo một view mới thay thế. ## Onboarding API đã bị deprecated \{#onboarding-api-deprecation\} Onboarding API cũ đã bị deprecated trong v4.0, thay thế bằng [Flow Builder](adapty-flow-builder). API này vẫn hoạt động bình thường, và IDE của bạn sẽ đánh dấu các ký hiệu deprecated thông qua annotation `@Deprecated` — không có cảnh báo nào ở runtime. Các ký hiệu này sẽ bị xóa trong bản phát hành tương lai, vì vậy hãy lên kế hoạch migrate các onboarding của bạn sang Flow Builder. Các ký hiệu không còn được hỗ trợ: `getOnboarding`, `getOnboardingForDefaultAudience`, `createOnboardingView`, `presentOnboardingView`, `dismissOnboardingView`, `setOnboardingsEventsObserver`, `AdaptyOnboarding`, `AdaptyUIOnboardingView`, `AdaptyUIOnboardingPlatformView`, `AdaptyUIOnboardingsEventsObserver`, và các model state, input, analytics của onboarding. --- # File: flutter-migration-guide-310 --- --- title: "Hướng dẫn migration lên Flutter Adapty SDK 3.10.0" description: "" --- Adapty SDK 3.10.0 là một bản phát hành lớn mang lại một số cải tiến, tuy nhiên có thể yêu cầu bạn thực hiện một số bước migration: 1. Cập nhật phương thức `makePurchase` để sử dụng `AdaptyPurchaseParameters` thay vì các tham số riêng lẻ. 2. Thay thế `vendorProductIds` bằng `productIdentifiers` trong model `AdaptyPaywall`. ## Cập nhật phương thức makePurchase \{#update-makepurchase-method\} Phương thức `makePurchase` hiện sử dụng `AdaptyPurchaseParameters` thay vì các đối số `subscriptionUpdateParams` và `isOfferPersonalized` riêng lẻ. Điều này giúp đảm bảo kiểu dữ liệu an toàn hơn và cho phép mở rộng các tham số mua hàng trong tương lai. ```diff showLineNumbers - final purchaseResult = await adapty.makePurchase( - product: product, - subscriptionUpdateParams: subscriptionUpdateParams, - isOfferPersonalized: true, - ); + final parameters = AdaptyPurchaseParametersBuilder() + ..setSubscriptionUpdateParams(subscriptionUpdateParams) + ..setIsOfferPersonalized(true) + ..setObfuscatedAccountId('your-account-id') + ..setObfuscatedProfileId('your-profile-id'); + final purchaseResult = await adapty.makePurchase( + product: product, + parameters: parameters.build(), + ); ``` Nếu không cần thêm tham số nào, bạn có thể sử dụng đơn giản như sau: ```dart showLineNumbers final purchaseResult = await adapty.makePurchase( product: product, ); ``` ## Cập nhật cách sử dụng model AdaptyPaywall \{#update-adaptywall-model-usage\} Thuộc tính `vendorProductIds` đã bị deprecated và được thay thế bằng `productIdentifiers`. Thuộc tính mới trả về các đối tượng `AdaptyProductIdentifier` thay vì chuỗi đơn giản, cung cấp thông tin sản phẩm có cấu trúc hơn. ```diff showLineNumbers - paywall.vendorProductIds.map((vendorId) => - ListTextTile(title: vendorId) - ).toList() + paywall.productIdentifiers.map((productId) => + ListTextTile(title: productId.vendorProductId) + ).toList() ``` Đối tượng `AdaptyProductIdentifier` cung cấp quyền truy cập vào vendor product ID thông qua thuộc tính `vendorProductId`, giữ nguyên chức năng như cũ trong khi cung cấp cấu trúc tốt hơn cho các cải tiến trong tương lai. ## Tương thích ngược \{#backward-compatibility\} Cả hai thay đổi đều duy trì tương thích ngược: - Các tham số cũ trong `makePurchase` đã bị deprecated nhưng vẫn hoạt động bình thường - Thuộc tính `vendorProductIds` đã bị deprecated nhưng vẫn có thể truy cập được - Code hiện tại sẽ tiếp tục hoạt động, mặc dù bạn sẽ thấy các cảnh báo deprecation Chúng tôi khuyến nghị cập nhật code của bạn để sử dụng các API mới nhằm đảm bảo khả năng tương thích trong tương lai và tận dụng tính an toàn kiểu dữ liệu và khả năng mở rộng được cải thiện. --- # File: flutter-migration-guide-38 --- --- title: "Migrate Adapty Flutter SDK to v3.8" description: "Migrate sang Adapty Flutter SDK v3.8 để cải thiện hiệu suất và có thêm tính năng monetization mới." --- Adapty SDK 3.8.0 là một bản phát hành lớn mang lại một số cải tiến, tuy nhiên có thể yêu cầu bạn thực hiện một số bước migration. 1. Cập nhật tên class observer và tên phương thức. 2. Cập nhật tên phương thức paywall dự phòng. 3. Cập nhật tên class view trong các phương thức xử lý sự kiện. ## Cập nhật tên class observer và tên phương thức \{#update-observer-class-and-method-names\} Tên class observer và phương thức đăng ký của nó đã được đổi tên: ```diff showLineNumbers - class MyObserver extends AdaptyUIObserver { + class MyObserver extends AdaptyUIPaywallsEventsObserver { @override void paywallViewDidPerformAction(AdaptyUIView view, AdaptyUIAction action) { // Handle action } } // Register observer - AdaptyUI().setObserver(this); + AdaptyUI().setPaywallsEventsObserver(this); ``` ## Cập nhật tên phương thức paywall dự phòng \{#update-fallback-paywalls-method-name\} Phương thức đặt paywall dự phòng đã được đơn giản hóa: ```diff showLineNumbers try { - await Adapty.setFallbackPaywalls(assetId); + await Adapty.setFallback(assetId); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` ## Cập nhật tên class view trong các phương thức xử lý sự kiện \{#update-view-class-name-in-event-handling-methods\} Tất cả các phương thức xử lý sự kiện hiện sử dụng class `AdaptyUIPaywallView` mới thay cho `AdaptyUIView`: ```diff showLineNumbers - void paywallViewDidPerformAction(AdaptyUIView view, AdaptyUIAction action) + void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) - void paywallViewDidSelectProduct(AdaptyUIView view, AdaptyPaywallProduct product) + void paywallViewDidSelectProduct(AdaptyUIPaywallView view, AdaptyPaywallProduct product) - void paywallViewDidStartPurchase(AdaptyUIView view, AdaptyPaywallProduct product) + void paywallViewDidStartPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product) - void paywallViewDidFinishPurchase(AdaptyUIView view, AdaptyPaywallProduct product, AdaptyProfile profile) + void paywallViewDidFinishPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyProfile profile) - void paywallViewDidFailPurchase(AdaptyUIView view, AdaptyPaywallProduct product, AdaptyError error) + void paywallViewDidFailPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyError error) - void paywallViewDidFinishRestore(AdaptyUIView view, AdaptyProfile profile) + void paywallViewDidFinishRestore(AdaptyUIPaywallView view, AdaptyProfile profile) - void paywallViewDidFailRestore(AdaptyUIView view, AdaptyError error) + void paywallViewDidFailRestore(AdaptyUIPaywallView view, AdaptyError error) - void paywallViewDidFailLoadingProducts(AdaptyUIView view, AdaptyIOSProductsFetchPolicy? fetchPolicy, AdaptyError error) + void paywallViewDidFailLoadingProducts(AdaptyUIPaywallView view, AdaptyIOSProductsFetchPolicy? fetchPolicy, AdaptyError error) - void paywallViewDidFailRendering(AdaptyUIView view, AdaptyError error) + void paywallViewDidFailRendering(AdaptyUIPaywallView view, AdaptyError error) ``` --- # File: migration-to-flutter-sdk-34 --- --- title: "Migrate Adapty Flutter SDK to v3.4" description: "Migrate to Adapty Flutter 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 với 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 các file paywall dự phòng \{#update-fallback-paywall-files\} 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ập nhật](fallback-paywalls) từ Adapty Dashboard. 2. [Thay thế các paywall dự phòng hiện có trong ứng dụng di động của bạn](flutter-use-fallback-paywalls) bằng các file mới. ## Cập nhật cách triển khai Observer Mode \{#update-implementation-of-observer-mode\} Nếu bạn đang sử dụng Observer Mode, hãy đảm bảo cập nhật cách triển khai của nó. Trước đây, các phương thức khác nhau được dùng để báo cáo giao dịch cho Adapty. Trong phiên bản mới, phương thức `reportTransaction` nên được sử dụng thống nhất trên cả Android và iOS. Phương thức này báo cáo rõ ràng từng giao dịch cho Adapty, đảm bảo nó được nhận diện. Nếu có sử dụng paywall, hãy truyền variation ID để liên kết giao dịch với paywall đó. :::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. ::: ```diff showLineNumbers - // every time when calling transaction.finish() - if (Platform.isAndroid) { - try { - await Adapty().restorePurchases(); - } on AdaptyError catch (adaptyError) { - // handle the error - } catch (e) { - } - } try { // every time when calling transaction.finish() await Adapty().reportTransaction( "YOUR_TRANSACTION_ID", variationId: "PAYWALL_VARIATION_ID", // optional ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` --- # File: migration-to-flutter330 --- --- title: "Migrate Adapty Flutter SDK sang v3.3" description: "Migrate sang Adapty Flutter SDK v3.3 để cải thiện hiệu suất và các tính năng monetization mới." --- Adapty SDK 3.3.0 là một bản phát hành lớn mang lại một số cải tiến, tuy nhiên có thể yêu cầu bạn thực hiện một số bước migration. 1. Cập nhật phương thức cung cấp paywall dự phòng. 2. Xóa phương thức `getProductsIntroductoryOfferEligibility`. 3. Cập nhật cấu hình tích hợp cho Adjust, AirBridge, Amplitude, AppMetrica, Appsflyer, Branch, Facebook Ads, Firebase and Google Analytics, Mixpanel, OneSignal, Pushwoosh. 4. Cập nhật cài đặt Observer mode. ## Cập nhật phương thức cung cấp paywall dự phòng \{#update-method-for-providing-fallback-paywalls\} Trước đây, phương thức yêu cầu paywall dự phòng dưới dạng chuỗi JSON (`jsonString`), nhưng bây giờ nó nhận đường dẫn đến file dự phòng cục bộ (`assetId`) thay thế. ```diff showLineNumbers import 'dart:async' show Future; import 'dart:io' show Platform; -import 'package:flutter/services.dart' show rootBundle; -final filePath = Platform.isIOS ? 'assets/ios_fallback.json' : 'assets/android_fallback.json'; -final jsonString = await rootBundle.loadString(filePath); +final assetId = Platform.isIOS ? 'assets/ios_fallback.json' : 'assets/android_fallback.json'; try { - await adapty.setFallbackPaywalls(jsonString); + await adapty.setFallbackPaywalls(assetId); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Để xem ví dụ code đầy đủ, hãy xem trang [Sử dụng paywall dự phòng](flutter-use-fallback-paywalls). ## 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 kiểm tra điều kiện thủ công trước khi sử dụng ưu đãi. Bây giờ, đối tượng sản phẩm chỉ bao gồm ưu đãi nếu 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 nữa — nếu có ưu đãi, người dùng đã đủ điều kiện. ## Cập nhật cấu hình SDK tích hợp bên thứ ba \{#update-third-party-integration-sdk-configuration\} Để đảm bảo các tích hợp hoạt động đúng với Adapty Flutter SDK 3.3.0 trở lên, hãy cập nhật cấu hình SDK của bạn cho các tích hợp sau theo mô tả trong các mục 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 import 'package:adjust_sdk/adjust.dart'; import 'package:adjust_sdk/adjust_config.dart'; try { final adid = await Adjust.getAdid(); if (adid == null) { // handle the error } + await Adapty().setIntegrationIdentifier( + key: "adjust_device_id", + value: adid, + ); final attributionData = await Adjust.getAttribution(); var attribution = Map(); if (attributionData.trackerToken != null) attribution['trackerToken'] = attributionData.trackerToken!; if (attributionData.trackerName != null) attribution['trackerName'] = attributionData.trackerName!; if (attributionData.network != null) attribution['network'] = attributionData.network!; if (attributionData.adgroup != null) attribution['adgroup'] = attributionData.adgroup!; if (attributionData.creative != null) attribution['creative'] = attributionData.creative!; if (attributionData.clickLabel != null) attribution['clickLabel'] = attributionData.clickLabel!; if (attributionData.costType != null) attribution['costType'] = attributionData.costType!; if (attributionData.costAmount != null) attribution['costAmount'] = attributionData.costAmount!.toString(); if (attributionData.costCurrency != null) attribution['costCurrency'] = attributionData.costCurrency!; if (attributionData.fbInstallReferrer != null) attribution['fbInstallReferrer'] = attributionData.fbInstallReferrer!; - Adapty().updateAttribution( - attribution, - source: AdaptyAttributionSource.adjust, - networkUserId: adid, - ); + await Adapty().updateAttribution(attribution, source: "adjust"); } catch (e) { // handle the error } on AdaptyError catch (adaptyError) { // handle the error } ``` ### 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 'package:airbridge_flutter_sdk/airbridge_flutter_sdk.dart'; final deviceUUID = await Airbridge.state.deviceUUID; try { - final builder = AdaptyProfileParametersBuilder() - ..setAirbridgeDeviceId(deviceUUID); - await Adapty().updateProfile(builder.build()); + await Adapty().setIntegrationIdentifier( + key: "airbridge_device_id", + value: deviceUUID, + ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // 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 'package:amplitude_flutter/amplitude.dart'; final Amplitude amplitude = Amplitude.getInstance(instanceName: "YOUR_INSTANCE_NAME"); final deviceId = await amplitude.getDeviceId(); final userId = await amplitude.getUserId(); try { - final builder = AdaptyProfileParametersBuilder() - ..setAmplitudeDeviceId(deviceId) - ..setAmplitudeUserId(userId); - await adapty.updateProfile(builder.build()); + await Adapty().setIntegrationIdentifier( + key: "amplitude_user_id", + value: userId, + ); + await Adapty().setIntegrationIdentifier( + key: "amplitude_device_id", + value: deviceId, + ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // 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 'package:appmetrica_plugin/appmetrica_plugin.dart'; final deviceId = await AppMetrica.deviceId; if (deviceId != null) { try { - final builder = AdaptyProfileParametersBuilder() - ..setAppmetricaDeviceId(deviceId) - ..setAppmetricaProfileId("YOUR_ADAPTY_CUSTOMER_USER_ID"); - - await adapty.updateProfile(builder.build()); + await Adapty().setIntegrationIdentifier( + key: "appmetrica_device_id", + value: deviceId, + ); + await Adapty().setIntegrationIdentifier( + key: "appmetrica_profile_id", + value: "YOUR_ADAPTY_CUSTOMER_USER_ID", + ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // 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 import 'package:appsflyer_sdk/appsflyer_sdk.dart'; AppsflyerSdk appsflyerSdk = AppsflyerSdk(); appsflyerSdk.onInstallConversionData((data) async { try { final appsFlyerUID = await appsFlyerSdk.getAppsFlyerUID(); - await Adapty().updateAttribution( - data, - source: AdaptyAttributionSource.appsflyer, - networkUserId: appsFlyerUID, - ); + await Adapty().setIntegrationIdentifier( + key: "appsflyer_id", + value: appsFlyerUID, + ); + + await Adapty().updateAttribution(data, source: "appsflyer"); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } }); appsflyerSdk.initSdk( registerConversionDataCallback: true, registerOnAppOpenAttributionCallback: true, registerOnDeepLinkingCallback: true, ); ``` ### 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 FlutterBranchSdk.initSession().listen((data) async { try { + await Adapty().setIntegrationIdentifier( + key: "branch_id", + value: , + ); - await Adapty().updateAttribution(data, source: AdaptyAttributionSource.branch); + await Adapty().updateAttribution(data, source: "branch"); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ); ``` ### Firebase and 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 final appInstanceId = await FirebaseAnalytics.instance.appInstanceId; try { - final builder = AdaptyProfileParametersBuilder() - ..setFirebaseAppInstanceId(appInstanceId); - await adapty.updateProfile(builder.build()); + await Adapty().setIntegrationIdentifier( + key: "firebase_app_instance_id", + value: appInstanceId, + ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // 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 final mixpanel = await Mixpanel.init("Your Token", trackAutomaticEvents: true); final distinctId = await mixpanel.getDistinctId(); try { - final builder = AdaptyProfileParametersBuilder() - ..setMixpanelUserId(distinctId); - await Adapty().updateProfile(builder.build()); + await Adapty().setIntegrationIdentifier( + key: "mixpanel_user_id", + value: distinctId, + ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // 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 OneSignal.shared.setSubscriptionObserver((changes) { final playerId = changes.to.userId; if (playerId != null) { - final builder = - AdaptyProfileParametersBuilder() - ..setOneSignalPlayerId(playerId); - // ..setOneSignalSubscriptionId(playerId); try { - Adapty().updateProfile(builder.build()); + await Adapty().setIntegrationIdentifier( + key: "one_signal_player_id", + value: playerId, + ); } on AdaptyError catch (adaptyError) { // handle error } catch (e) { // handle error } } }); ``` ### 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 final hwid = await Pushwoosh.getInstance.getHWID; - final builder = AdaptyProfileParametersBuilder() - ..setPushwooshHWID(hwid); try { - await adapty.updateProfile(builder.build()); + await Adapty().setIntegrationIdentifier( + key: "pushwoosh_hwid", + value: hwid, + ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` ## Cập nhật cài đặt Observer mode \{#update-observer-mode-implementation\} Cập nhật cách bạn liên kết paywall với các giao dịch. Trước đây, bạn dùng phương thức `setVariationId` để gán `variationId`. Bây giờ, 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 đầy đủ tại [Liên kết paywall với giao dịch mua trong Observer mode](report-transactions-observer-mode-flutter). :::warning Đừng quên ghi lại giao dịch bằng phương thức `reportTransaction`. Bỏ qua bước này đồng nghĩa với việc Adapty sẽ không nhận diện được giao dịch, không cấp mức độ truy cập, không đưa vào analytics và không gửi đến các tích hợp. Bước này là bắt buộc! ::: ```diff showLineNumbers try { - await Adapty().setVariationId("YOUR_TRANSACTION_ID", "PAYWALL_VARIATION_ID"); + // every time when calling transaction.finish() + await Adapty().reportTransaction( + "YOUR_TRANSACTION_ID", + variationId: "PAYWALL_VARIATION_ID", // optional + ); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` --- # File: migration-to-flutter-sdk-v3 --- --- title: "Migrate Adapty Flutter SDK to v3.0" description: "Migrate sang Adapty Flutter 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) phiên bản mới — công cụ no-code thân thiện với người dùng để tạo paywall. Với sự 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. ::: ## Gỡ bỏ AdaptyUI SDK \{#remove-adaptyui-sdk\} 1. AdaptyUI trở thành một module trong Adapty SDK, vì vậy hãy xóa `adapty_ui_flutter` khỏi file `pubspec.yaml` của bạn: ```diff showLineNumbers dependencies: + adapty_flutter: ^3.2.1 - adapty_flutter: ^2.10.3 - adapty_ui_flutter: ^2.1.3 ``` 2. Chạy lệnh: ```bash showLineNumbers title="Bash" flutter pub get ``` ## Cấu hình Adapty SDK \{#configure-adapty-sdks\} Trước đây, bạn cần sử dụng file `Adapty-Info.plist` và `AndroidManifest.xml` để cấu hình Adapty SDK. Bây giờ, bạn không cần dùng các file bổ sung nữa. Thay vào đó, bạn có thể cung cấp tất cả các tham số cần thiết trong quá trình kích hoạt. Bạn chỉ cần cấu hình Adapty SDK một lần, thường là khi khởi động ứng dụng. ### Kích hoạt module Adapty của Adapty SDK \{#activate-adapty-module-of-adapty-sdk\} 1. Xóa import AdaptyUI SDK khỏi ứng dụng của bạn như sau: ```diff showLineNumbers import 'package:adapty_flutter/adapty_flutter.dart'; - import 'package:adapty_ui_flutter/adapty_ui_flutter.dart'; ``` 2. Cập nhật cách kích hoạt Adapty SDK như sau: ```diff showLineNumbers try { - Adapty().activate(); + await Adapty().activate( + configuration: AdaptyConfiguration(apiKey: 'YOUR_API_KEY') + ..withLogLevel(AdaptyLogLevel.debug) + ..withObserverMode(false) + ..withCustomerUserId(null) + ..withIdfaCollectionDisabled(false) + ..withIpAddressCollectionDisabled(false), + ); } catch (e) { // handle the error } ``` Các tham số: | Tham số | Bắt buộc | Mô tả | | ----------------------------------- | -------- | ------------------------------------------------------------ | | **PUBLIC_SDK_KEY** | bắt buộc | Key bạn có thể tìm thấy trong trường **Public SDK key** của cài đặt ứng dụng trong Adapty: [**App settings** -> tab **General** -> mục **API keys**](https://app.adapty.io/settings/general) | | **withLogLevel** | tùy chọn | Adapty ghi lại các lỗi và thông tin quan trọng để cung cấp thông tin chi tiết về hoạt động của ứng dụng. Các cấp độ log có sẵn:
  • error: Chỉ ghi lại các lỗi.
  • warn: Ghi lại 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 lại các lỗi, cảnh báo và thông báo thông tin quan trọng, chẳng hạn như vòng đời của các module.
  • verbose: Ghi lại mọi thông tin bổ sung có thể hữu ích khi debug, chẳng hạn như các lần gọi hàm, API query, v.v.
| | **withObserverMode** | tùy chọn |

Giá trị boolean kiểm soát [Observer mode](observer-vs-full-mode). Bật tùy chọn này nếu bạn tự xử lý giao dịch mua và trạng thái gói đăng ký, và sử dụng Adapty để gửi sự kiện gói đăng ký và analytics.

Giá trị mặc định là `false`.

🚧 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ý việc này.

| | **withCustomerUserId** | tùy chọn | Mã định danh người dùng trong hệ thống của bạn. Chúng tôi gửi nó trong các sự kiện gói đăng ký và analytics để gán sự kiện đúng với hồ sơ người dùng. Bạn cũng có thể tìm kiếm người dùng theo `customerUserId` trong menu [**Profiles and Segments**](https://app.adapty.io/profiles/users). | | **withIdfaCollectionDisabled** | tùy chọn |

Đặt thành `true` để tắt tính năng thu thập và chia sẻ IDFA.

Chia sẻ địa chỉ IP của người dùng.

Giá trị mặc định là `false`.

Để biết thêm chi tiết về việc thu thập IDFA, hãy xem phần [Tích hợp Analytics](analytics-integration#disable-collection-of-advertising-identifiers).

| | **withIpAddressCollectionDisabled** | tùy chọn |

Đặt thành `true` để tắt tính năng thu thập và chia sẻ địa chỉ IP của người dùng.

Giá trị mặc định là `false`.

| ### Kích hoạt module AdaptyUI của Adapty SDK \{#activate-adaptyui-module-of-adapty-sdk\} Bạn chỉ cần cấu hình module AdaptyUI nếu có kế hoạch sử dụng [Paywall Builder](adapty-paywall-builder): ```dart showLineNumbers title="Dart" try { final mediaCache = AdaptyUIMediaCacheConfiguration( memoryStorageTotalCostLimit: 100 * 1024 * 1024, // 100MB memoryStorageCountLimit: 2147483647, // 2^31 - 1, max int value in Dart diskStorageSizeLimit: 100 * 1024 * 1024, // 100MB ); await AdaptyUI().activate( configuration: AdaptyUIConfiguration(mediaCache: mediaCache), observer: , ); } catch (e) { // handle the error } ``` 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 sử dụng config, tất cả các tham số trong đó đều là bắt buộc. Các tham số: | Tham số | Bắt buộc | Mô tả | | :------------------------------ | :------- | :----------------------------------------------------------- | | **memoryStorageTotalCostLimit** | bắt buộc | Giới hạn tổng dung lượng lưu trữ tính bằng byte. | | **memoryStorageCountLimit** | bắt buộc | Giới hạn số lượng item trong bộ nhớ lưu trữ. | | **diskStorageSizeLimit** | bắt buộc | Giới hạn kích thước file trên ổ đĩa tính bằng byte. 0 nghĩa là không giới hạn. | --- # End of Documentation _Generated on: 2026-07-24T13:01:43.439Z_ _Successfully processed: 44/44 files_