# FLUTTER - Adapty Documentation (Full Content) This file contains the complete content of all documentation pages for this platform. Locale: vi Generated on: 2026-07-01T16:30:13.626Z Total files: 42 --- # 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 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**: SDK cốt lõi, bắt buộc phải có để Adapty hoạt động đúng trong ứng dụng. - **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 để 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 cài đặt, 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 cần iOS 15.0+ để hoạt động đúng với các paywall được tạo trong Paywall Builder. :::info Adapty tương thích với Google Play Billing Library đến phiên bản 8.x. 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ể thêm thủ công [dependency](https://developer.android.com/google/play/billing/integrate#dependency). ::: --- no_index: true --- import Callout from '../../../components/Callout.astro'; Cài đặt SDK là bước 5 trong quá trình thiết lập Adapty. Trước khi các giao dịch mua hàng hoạt động trong ứng dụng, bạn cần kết nối ứng dụng với các cửa hàng, sau đó tạo sản phẩm, paywall và placement trong Adapty Dashboard. [Hướng dẫn quickstart](quickstart) sẽ hướng dẫn bạn qua tất cả các bước cần thiết. ## Cài đặt Adapty SDK \{#install-adapty-sdk\} [![Release](https://img.shields.io/github/v/release/adaptyteam/AdaptySDK-Flutter.svg?style=flat&logo=flutter)](https://github.com/adaptyteam/AdaptySDK-Flutter/releases) 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 dependency: ```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'; ``` ## 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. ::: Để 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 dùng [Adapty Paywall Builder](adapty-paywall-builder), trước tiên hãy [kích hoạt module AdaptyUI](#activate-adaptyui-module-of-adapty-sdk) bên dưới, sau đó làm theo [hướng dẫn nhanh Paywall Builder](flutter-quickstart-paywalls). - Nếu bạn tự xây dựng giao diện paywall, hãy 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 dự định 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 được liên kết vào ứng dụng bất kể AdaptyUI có được kích hoạt hay không. ::: :::important Trong code của bạn, phải kích hoạt module Adapty cốt lõi 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 để giúp bạn hiểu những gì đang xảy ra. Các mức log có sẵn như sau: | Mức | Mô tả | | :----------------------- | :------------------------------------------------------------------------------------------------------------------------ | | `AdaptyLogLevel.none` | Không ghi log gì cả. Giá trị mặc định | | `AdaptyLogLevel.error` | Chỉ ghi log các lỗi | | `AdaptyLogLevel.warn` | Ghi log các lỗi và thông báo từ SDK không gây ra lỗi nghiêm trọng nhưng đáng chú ý. | | `AdaptyLogLevel.info` | Ghi log các lỗi, cảnh báo và các thông báo thông tin khác nhau. | | `AdaptyLogLevel.verbose` | Ghi log mọi thông tin bổ sung có thể hữu ích trong quá trình debug, như lời gọi hàm, truy vấn API, v.v. | 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 gửi dữ liệu đó một cách tường minh, nhưng bạn có thể áp dụng thêm các chính sách bảo mật dữ liệu để tuân thủ quy định của cửa hàng hoặc quốc gia. #### Tắt thu thập và chia sẻ địa chỉ IP \{#disable-ip-address-collection-and-sharing\} Khi kích hoạt module Adapty, đặt `ipAddressCollectionDisabled` thành `true` để tắt việc 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ư 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 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 IP không được yêu cầu cho ứ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 thu thập và chia sẻ ID quảng cáo \{#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 các identifier 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 App Store/Play Store, tránh kích hoạt lời nhắc App Tracking Transparency, hoặc khi ứng dụng của bạn không cần attribution quảng cáo hay analytics 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 ); ``` #### Thiết lập cấu hình cache media cho AdaptyUI \{#set-up-media-cache-configuration-for-adaptyui\} Module đượ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. Mặc định, AdaptyUI lưu cache media (như hình ảnh và video) để cải thiện hiệu suất và giảm sử dụng mạng. Bạn có thể tùy chỉnh cài đặt cache bằng cách cung cấp cấu hình riêng. Sử dụng `withMediaCacheConfiguration` để ghi đè kích thước cache và thời gian hiệu lực 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 sử dụng cấu hình, tất cả các tham số phải được bao gồm. ```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 | tùy chọn | Tổng kích thước cache trong bộ nhớ tính bằng byte. Mặc định là 100 MB. | | memoryStorageCountLimit | tùy chọn | Giới hạn số lượng item trong bộ nhớ. Mặc định là giá trị int tối đa. | | diskStorageSizeLimit | tùy chọn | 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\} 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, đặ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 backup \{#clear-data-on-backup-restore\} Khi `clearDataOnBackup` được đặt thành `true`, SDK sẽ phát hiện khi ứng dụng được khôi phục từ backup iCloud và xóa toàn bộ dữ liệu SDK được lưu cục bộ, bao gồm thông tin hồ sơ người dùng được cache, chi tiết sản phẩm và paywall. SDK sau đó 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ó cache SDK cục bộ 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') ..withClearDataOnBackup(true) // default – false ); ``` ## Xử lý sự cố \{#troubleshooting\} #### Quy tắc backup 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" #### Mua hàng 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 flow 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ả mua hàng bị mất hoặc bị coi là đã hủy. Để đảm bảo mua hàng hoạt động đúng, chỉ sử dụng launch mode `standard` hoặc `singleTop` cho Activity khởi động flow mua hàng, và tránh các mode khác. Trong `AndroidManifest.xml`, hãy đảm bảo Activity khởi động 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ể thấy lỗi biên dịch Swift 6 trên các pod target của Adapty. Các triệu chứng thường gặp bao gồm lỗi `@Sendable` trong `AdaptyUIBuilderLogic`, thiếu conformance `Sendable` trên các kiểu Adapty, hoặc lỗi actor isolation. Các pod Adapty khai báo `s.swift_version = '6.0'` và yêu cầu Swift 6 để build. Code ứng dụng của bạn có thể tiếp tục dùng Swift 5 — chỉ các pod target của Adapty (`Adapty`, `AdaptyUI`, `AdaptyUIBuilder`, `AdaptyLogger`, `AdaptyPlugin`) cần được 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 mọi 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 việc 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 pod target `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 bằng cách sử dụng paywall trong Flutter SDK" description: "Hướng dẫn nhanh để thiết lập Adapty cho việc quản lý gói đăng ký trong ứng dụng." --- Để kích hoạt 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) - [**Paywall**](paywalls) là các cấu hình xác định sản phẩm nào sẽ được cung cấp. Trong Adapty, paywall là cách duy nhất để lấy sản phẩm, nhưng thiết kế này cho phép bạn thay đổi ưu đãi, giá cả và tổ hợp sản phẩm mà không cần chỉnh sửa code của ứng dụng. - [**Placement**](placements) – nơi và thời điểm bạn hiển thị paywall trong ứng dụng (ví dụ: `main`, `onboarding`, `settings`). Bạn thiết lập paywall cho các placement trên dashboard, sau đó yêu cầu chúng bằng placement ID trong code. Điều 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 các nhóm người dùng khác nhau. Adapty cung cấp ba cách để kích hoạt mua hàng trong ứng dụng của bạn. Hãy chọn một trong số đó tùy theo yêu cầu của ứng dụng: | Cách triển khai | Độ phức tạp | Khi nào dùng | |------------------------|------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Adapty Paywall Builder | ✅ Dễ | Bạn [tạo một paywall hoàn chỉnh, sẵn sàng để mua hàng trong no-code builder](quickstart-paywalls). Adapty tự động render và xử lý toàn bộ quy trình mua hàng phức tạp, xác thực biên lai và quản lý gói đăng ký ở phía sau. | | Paywall tạo thủ công | 🟡 Trung bình | Bạn tự triển khai giao diện paywall trong code ứng dụng, nhưng vẫn lấy đối tượng paywall từ Adapty để duy trì sự linh hoạt trong việc cung cấp sản phẩm. Xem [hướng dẫn](flutter-quickstart-manual). | | Chế độ Observer | 🔴 Khó | Bạn đã có cơ sở hạ tầng xử lý mua hàng riêng và muốn tiếp tục sử 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 paywall được tạo trong Adapty paywall builder.** Nếu bạn không muốn sử dụng paywall builder, hãy xem [hướng dẫn xử lý mua hàng trong paywall tạo thủ công](flutter-making-purchases). ::: Để hiển thị paywall được tạo trong Adapty paywall builder, trong code ứng dụng, bạn chỉ cần: 1. **Lấy paywall**: Lấy paywall từ Adapty. 2. **Hiển thị paywall và Adapty sẽ xử lý mua hàng cho bạn**: Hiển thị container paywall mà bạn đã lấy trong ứng dụng. 3. **Xử lý hành động nút**: Liên kết tương tác của người dùng với paywall với phản hồi của ứng dụng. Ví dụ: mở liên kết hoặc đóng paywall 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 paywall và thêm sản phẩm vào đó](create-paywall). 4. [Tạo placement và thêm paywall vào đó](create-placement). 5. [Cài đặt và kích hoạt SDK](sdk-installation-flutter) trong code ứng dụng của bạn. :::tip Cách nhanh nhất để hoàn thành các bước này là làm theo [hướng dẫn nhanh](quickstart) hoặc tạo paywall và placement bằng [Developer CLI](developer-cli-quickstart). ::: ## 1. Lấy paywall \{#1-get-the-paywall\} Các paywall của bạn được liên kết với các placement được cấu hình trên dashboard. Placement cho phép bạn chạy các paywall khác nhau cho các đối tượng khác nhau hoặc chạy [A/B test](ab-tests). Để lấy paywall được tạo trong Adapty paywall builder, bạn cần: 1. Lấy đối tượng `paywall` theo [placement](placements) ID bằng phương thức `getPaywall` và kiểm tra xem đó có phải là paywall được tạo trong builder hay không thông qua thuộc tính `hasViewConfiguration`. 2. Tạo paywall view bằng phương thức `createPaywallView`. View chứa các phần tử giao diện và kiểu dáng cần thiết để hiển thị paywall. :::important Để lấy cấu hình view, bạn phải bật toggle **Show on device** trong Paywall Builder. Nếu không, bạn sẽ nhận được cấu hình view rỗng và paywall sẽ không được hiển thị. ::: ```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) { } try { final view = await AdaptyUI().createPaywallView( paywall: paywall, ); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` ## 2. Hiển thị paywall \{#2-display-the-paywall\} Khi bạn đã có cấu hình paywall, chỉ cần thêm vài dòng code là đủ để hiển thị paywall của mình. Để hiển thị paywall, sử dụng phương thức `view.present()` trên `view` được tạo bởi phương thức `createPaywallView`. 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 một instance `view` 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ị paywall, hãy xem [hướng dẫn](flutter-present-paywalls) của chúng tôi. ::: ## 3. Xử lý hành động nút \{#3-handle-button-actions\} Khi người dùng nhấn các nút trong paywall, Flutter SDK tự động xử lý việc mua hàng và khôi phục. Tuy nhiên, các nút khác có ID tùy chỉnh hoặc được định nghĩa sẵn và yêu cầu xử lý hành động trong code của bạn. Để kiểm soát hoặc theo dõi các tiến trình trên màn hình paywall, hãy triển khai 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. Nếu người dùng đã thực hiện một hành động nào đó, `paywallViewDidPerformAction` sẽ được gọi và ứng dụng của bạn cần phản hồi tùy theo action ID. Ví dụ: paywall của bạn có thể có nút đóng và các URL cần mở (ví dụ: điều khoản sử dụng và chính sách bảo mật). Vì vậy, bạn cần phản hồi các hành động với ID `Close` và `OpenUrl`. :::tip Đọc hướng dẫn của chúng tôi về cách xử lý [hành động](flutter-handle-paywall-actions) và [sự kiện](flutter-handling-events) nút. ::: ```dart showLineNumbers title="Flutter" class _PaywallScreenState extends State implements AdaptyUIPaywallsEventsObserver { @override void initState() { super.initState(); // Register this class as the paywalls event observer AdaptyUI().setPaywallsEventsObserver(this); } // This method is called when user performs an action on the paywall UI @override void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) { switch (action) { case const CloseAction(): case const AndroidSystemBackAction(): view.dismiss(); break; case OpenUrlAction(url: final url): // Open the URL using url_launcher package _launchUrl(url); break; } } // Helper method to launch URLs Future _launchUrl(String url) async { try { final Uri uri = Uri.parse(url); if (await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); } else { // Handle case where URL cannot be launched print('Could not launch $url'); } } catch (e) { // Handle any errors print('Error launching URL: $e'); } } } ``` ## Bước tiếp theo \{#next-steps\} --- no_index: true --- import Callout from '../../../components/Callout.astro'; Bạn có câu hỏi hoặc gặp sự cố? Hãy xem [diễn đàn hỗ trợ](https://adapty.featurebase.app/) của chúng tôi — nơi bạn có thể tìm câu trả lời cho các câu hỏi thường gặp hoặc đặt câu hỏi của riêng mình. Đội ngũ và cộng đồng của chúng tôi luôn sẵn sàng giúp đỡ! Paywall của bạn đã sẵn sàng để hiển thị trong ứng dụng. 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\} Đây là cách tất cả các bước đó có thể được tích hợp cùng nhau trong ứng dụng của bạn. ```dart void main() async { runApp(MaterialApp(home: PaywallScreen())); } class PaywallScreen extends StatefulWidget { @override State createState() => _PaywallScreenState(); } class _PaywallScreenState extends State implements AdaptyUIPaywallsEventsObserver { @override void initState() { super.initState(); // Register this class as the paywalls event observer AdaptyUI().setPaywallsEventsObserver(this); _showPaywallIfNeeded(); } Future _showPaywallIfNeeded() async { try { final paywall = await Adapty().getPaywall( placementId: 'YOUR_PLACEMENT_ID', ); if (!paywall.hasViewConfiguration) return; final view = await AdaptyUI().createPaywallView(paywall: paywall); await view.present(); } catch (_) { // Handle any errors (network, SDK issues, etc.) } } // This method is called when user performs an action on the paywall UI @override void paywallViewDidPerformAction(AdaptyUIPaywallView view, AdaptyUIAction action) { switch (action) { case const CloseAction(): case const AndroidSystemBackAction(): view.dismiss(); break; case OpenUrlAction(url: final url): // Open the URL using url_launcher package _launchUrl(url); break; } } // Helper method to launch URLs Future _launchUrl(String url) async { try { final Uri uri = Uri.parse(url); if (await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); } else { // Handle case where URL cannot be launched print('Could not launch $url'); } } catch (e) { // Handle any errors print('Error launching URL: $e'); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Adapty Paywall Example')), body: Center( // Add a button to re-trigger the paywall for testing purposes child: ElevatedButton( onPressed: _showPaywallIfNeeded, child: Text('Show Paywall'), ), ), ); } } ``` --- # 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`: ```javascript showLineNumbers try { final profile = await Adapty().getProfile(); // check the access } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` ### Lắng nghe cập nhật gói đăng ký \{#listen-to-subscription-updates\} Để tự động nhận các cập nhật hồ sơ trong ứng dụng: 1. Dùng `Adapty().didUpdateProfileStream.listen()` để lắng nghe các thay đổi hồ sơ — 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ơ được cập nhật khi phương thức này được gọi, để bạn có thể sử dụng xuyên suốt ứng dụng mà không cần thực hiện thêm request mạng. ```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ơ khi ứng dụng khởi động, cung cấp dữ liệu gói đăng ký đã lưu cache ngay cả khi thiết bị không có kết nối mạng. ::: ## Kết nối hồ sơ với logic paywall \{#connect-profile-with-paywall-logic\} Khi 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 nội dung trả phí, bạn có thể kiểm tra trực tiếp hồ sơ người dùng. Cách này hữu ích trong các tình huống như khi khởi động ứng dụng, khi vào các mục 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. ::: --- no_index: true --- [Skill adapty-sdk-integration](https://github.com/adaptyteam/adapty-sdk-integration-skill) tự động hóa toàn bộ quá trình tích hợp Adapty: thiết lập dashboard, cài đặt SDK, paywall và xác minh từng giai đoạn. Skill tự động nhận diện nền tảng của bạn và tải tài liệu Adapty phù hợp ở mỗi giai đoạn. **Công cụ được hỗ trợ**: Claude Code, GitHub Copilot CLI, OpenAI Codex, Gemini CLI. Để cài đặt, chọn lệnh phù hợp với công cụ của bạn. Danh sách đầy đủ có trong [README của skill](https://github.com/adaptyteam/adapty-sdk-integration-skill). **Claude Code** ``` claude plugin marketplace add adaptyteam/adapty-sdk-integration-skill claude plugin install adapty-sdk-integration@adapty ``` **GitHub Copilot CLI** ``` gh skill install adaptyteam/adapty-sdk-integration-skill ``` **Gemini CLI** ``` gemini skills install https://github.com/adaptyteam/adapty-sdk-integration-skill ``` **OpenAI Codex hoặc bất kỳ công cụ nào khác**: Clone repo và sao chép thư mục `plugins/adapty-sdk-integration/skills/adapty-sdk-integration/` vào thư mục skills của công cụ bạn đang dùng. Sau khi cài đặt, chạy skill trong dự án của bạn: ``` /adapty-sdk-integration ``` Skill sẽ hỏi một vài câu hỏi thiết lập, sau đó hướng dẫn bạn qua các bước: thiết lập dashboard, cài đặt SDK, paywall và xác minh. --- # File: adapty-cursor-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 paywall và cấu hình của chúng bằng Paywall Builder trong Flutter SDK" description: "Tìm hiểu cách lấy paywall PB trong Adapty để kiểm soát gói đăng ký tốt hơn trong Flutter." --- Sau khi [bạn đã thiết kế phần giao diện cho paywall](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 là lấy paywall gắn với placement và cấu hình view của nó như mô tả bên dưới. :::warning Paywall Builder mới yêu cầu Flutter SDK phiên bản 3.3.0 trở lên. ::: 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 tự triển khai paywall theo cách 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 di động](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 di động (nhấn để mở rộng) 1. [Tạo sản phẩm](create-product) trong Adapty Dashboard. 2. [Tạo paywall và thêm sản phẩm vào đó](create-paywall) trong Adapty Dashboard. 3. [Tạo placement và thêm paywall vào đó](create-placement) trong Adapty Dashboard. 4. Cài đặt [Adapty SDK](sdk-installation-flutter) trong ứ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 chứa cả nội dung hiển thị lẫn cách thức hiển thị. Tuy nhiên, bạn vẫn cần lấy ID của nó thông qua placement, cấu hình view của nó, rồi hiển thị trong ứng dụng di động. Để đảm bảo hiệu suất tối ưu, hãy 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, để ảnh có đủ thời gian tải xuống trước khi hiển thị cho người dùng. Để lấy paywall, 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 là mã ngôn ngữ gồm một hoặc hai thẻ phụ được phân cách bằng dấu trừ (**-**). Thẻ phụ đầu tiên là ngôn ngữ, thẻ thứ hai là vùng.

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

Xem [Bản địa hóa và mã locale](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ị 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ó 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 có kém đến đâu. Cache được cập nhật thường xuyên nên an toàn khi dùng trong phiên để tránh các yêu cầu mạng.

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

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 dùng CDN để tải paywall nhanh hơn và máy chủ dự phòng độc lập trong trường hợp CDN không tiếp cận được. 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 |

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 `loadTimeout` đã chỉ định, vì thao tác có thể bao gồm nhiều yêu cầu 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` đến từ `import com.adapty.utils.seconds`), hoặc `TimeInterval.seconds(5)`. Để không giới hạn thời gian, dùng `TimeInterval.INFINITE`.

| Tham số phản hồi: | Tham số | Mô tả | | :-------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | | Paywall | Một đối tượng [`AdaptyPaywall`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html) với danh sách ID sản phẩm, định danh paywall, Remote Config và một số thuộc tính khác. | ## Lấy cấu hình view 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 view sẽ không thể lấy được. ::: Sau khi lấy paywall, hãy kiểm tra xem nó có chứa `ViewConfiguration` hay không — điều này cho biết paywall được tạo bằng Paywall Builder. Thông tin này sẽ hướng dẫn bạn cách hiển thị paywall. Nếu có `ViewConfiguration`, 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, [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 lấy 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à paywall, và người dùng có kết nối internet yếu, việc lấy 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ị 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. Để giải quyết điều này, bạn có thể dùng phương thức `getPaywallForDefaultAudience`, phương thức này lấy 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 paywall bằng phương thức `getPaywall`, như đã mô tả trong phần [Lấy thông tin paywall](flutter-get-pb-paywalls#fetch-paywall-designed-with-paywall-builder) ở trên. :::warning Lý do chúng tôi khuyến nghị dùng `getPaywall` Phương thức `getPaywallForDefaultAudience` có một số hạn chế đáng kể: - **Vấn đề tương thích ngược 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ế paywall hỗ trợ phiên bản hiện tại (cũ) hoặc chấp nhận rằng người dùng dùng phiên bản hiện tại (cũ) có thể gặp sự cố với paywall không render đượ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 bạn). Nếu bạn sẵn sàng chấp nhận những hạn chế này để được hưởng lợi từ việc lấy paywall nhanh hơn, dùng phương thức `getPaywallForDefaultAudience` như sau. Ngược lại, hãy 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` có sẵn 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ẻ phụ được phân cách bằng dấu trừ (**-**). Thẻ phụ đầu tiên là ngôn ngữ, thẻ thứ hai là vùng.

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

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

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

Theo mặc định, SDK sẽ cố tải dữ liệu từ máy chủ và trả về dữ liệu đã cache 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ó 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 có kém đến đâu. Cache được cập nhật thường xuyên nên an toàn khi dùng trong phiên để tránh các yêu cầu mạng.

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

| ## Tùy chỉnh assets \{#customize-assets\} Để tùy chỉnh hình ảnh và video trong paywall, hãy triển khai custom assets. Hình ảnh hero và video có ID được xác định trước: `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 preview cục bộ trong khi hình ảnh chính từ xa đang tải. - Hiển thị hình ảnh preview trước khi chạy 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. ::: Đây là ví dụ về cách 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: , preloadProducts: preloadProducts, ); } on AdaptyError catch (e) { // handle the error } catch (e) { // handle the error } ``` :::note Nếu không tìm thấy asset, paywall sẽ hiển thị theo 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 tạo một đối tượng tuân theo giao thức `AdaptyTimerResolver`. Đối tượng này định nghĩa cách mỗi custom timer sẽ được render. Nếu muốn, bạn có thể dùng trực tiếp dictionary `[String: Date]` vì nó đã tuân thủ giao thức này. Đâ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à **Timer ID** của các timer do nhà phát triển định nghĩa mà bạn đã thiết lập trong Adapty Dashboard. `timerResolver` đảm bảo ứng dụng của bạn cập nhật động mỗi timer với giá trị chính xác. Ví dụ: - `CUSTOM_TIMER_NY`: Thời gian còn lại cho đến khi timer kết thúc, chẳng hạn như Năm mới. - `CUSTOM_TIMER_6H`: Thời gian còn lại trong khoảng thời gian 6 giờ bắt đầu khi người dùng mở paywall. --- # File: flutter-present-paywalls --- --- title: "Flutter - Present new Paywall Builder paywalls" description: "Hiển thị paywall trong ứng dụng Flutter bằng các tính năng kiếm tiền của Adapty." --- Nếu bạn đã tùy chỉnh paywall bằng Paywall Builder, bạn không cần phải lo lắng về việc render nó trong code ứng dụng để hiển thị cho người dùng. Paywall như vậy đã bao gồm cả nội dung cần hiển thị lẫn cách thức hiển thị. :::warning Hướng dẫn này chỉ dành cho **paywall Paywall Builder mới** — yêu cầu SDK v3.2.0 trở lên. Cách hiển thị paywall khác nhau tùy theo phiên bản Paywall Builder được dùng để thiết kế và loại paywall Remote Config. - Để hiển thị **paywall Remote Config**, xem [Render paywall được thiết kế bằng remote config](present-remote-config-paywalls). ::: 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ị paywall dưới dạng màn hình độc lập, 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ể dùng một lần. Nếu cần hiển thị lại paywall, hãy gọi `createPaywallView` thêm một lần nữa để tạo `view` mới. :::warning 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, 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 alert thông thường khi một paywall view đang được hiển thị trên Android. Trên Android, các alert thông thường xuất hiện phía sau paywall view, khiến người dùng không thể nhìn thấy chúng. Phương thức này đảm bảo hộp thoại hiển thị đúng vị trí phía trên paywall trên tất cả các 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 chấp 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ó, 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 Để platform view trên Android 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) } } ``` ::: --- # File: flutter-handle-paywall-actions --- --- title: "Xử lý hành động nút trong Flutter SDK" description: "Xử lý hành động nút paywall trong Flutter bằng Adapty để tối ưu hóa việc kiếm tiền từ ứng dụng." --- Nếu bạn đang xây dựng paywall bằng Adapty Paywall Builder, việc thiết lập các nút đúng cách là rất quan trọng: 1. Thêm [nút trong Paywall Builder](paywall-buttons) và gán cho nó một 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 đã gán. Hướng dẫn này hướng dẫn cách xử lý các hành động tùy chỉnh và hành động có sẵn trong code của bạn. :::warning **Chỉ có giao dịch mua và khôi phục đượ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 yêu cầu triển khai xử lý phù hợp trong code ứng dụng. ::: ## Đóng paywall \{#close-paywalls\} Để thêm nút đóng paywall: 1. Trong Paywall Builder, thêm một nút và gán cho nó hành động **Close**. 2. Trong code ứng dụng, triển khai trình xử lý 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ở 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 mua), hãy thêm phần tử **Link** trong Paywall Builder và xử lý nó giống như nút với hành động **Open URL**. ::: Để thêm nút mở liên kết từ paywall (ví dụ: **Điều khoản sử dụng** hoặc **Chính sách bảo mật**): 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, triển khai trình xử lý 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(AdaptyUIView 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 đăng nhập người dùng 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 trình xử lý 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'): // Handle login action Navigator.of(context).push(MaterialPageRoute(builder: (context) => LoginScreen())); break; default: break; } } ``` ## Xử lý hành động tùy chỉnh \{#handle-custom-actions\} Để thêm nút xử lý các hành động khác: 1. Trong Paywall Builder, thêm một nút, gán cho nó hành động **Custom**, và đặt cho nó một ID. 2. Trong code ứng dụng, triển khai trình xử lý 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ý khác hoặc sản phẩm mua một lần, bạn có thể thêm nút để hiển thị một 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 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, chọn sản phẩm và hiển thị paywall. Bạn cũng cần triển khai xử lý nút bấm (đóng paywall, mở liên kết, v.v.). Xem [hướng dẫn xử lý hành động nút bấm](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 thao tác nhấn nút (nút đóng, URL, chọn sản phẩm, v.v.) cũng như thông báo về các hành động liên quan đến giao dịch mua trên paywall. Tìm hiểu 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 tiến trình diễn ra trên màn hình paywall trong ứng dụng di động của bạn, hãy triển khai các phương thức `AdaptyUIPaywallsEventsObserver` và thiết lập observer trước khi hiển thị bất kỳ màn hình nào: ```javascript 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. ::: ### 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 được 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 vào [nút web paywall](web-paywall#step-2a-add-a-web-purchase-button) bên trong paywall và một web paywall mở ra trong trình duyệt trong ứng dụng. ::: ```javascript 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) được mở từ paywall trong trình duyệt trong ứng dụng biến mất khỏi màn hình. ::: ```javascript 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 hệ thống), phương thức này sẽ được gọi: ```javascript showLineNumbers title="Flutter" void paywallViewDidSelectProduct(AdaptyUIPaywallView view, String productId) { } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```javascript { "productId": "premium_monthly" } ```
#### Bắt đầu mua \{#started-purchase\} Nếu người dùng khởi tạo quá trình mua, phương thức này sẽ được gọi: ```javascript showLineNumbers title="Flutter" void paywallViewDidStartPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product) { } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```javascript { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" } } ```
#### Hoàn thành mua \{#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 mua, hoặc giao dịch mua đang ở trạng thái chờ xử lý: ```javascript 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) ```javascript // Successful purchase { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "purchaseResult": { "type": "AdaptyPurchaseResultSuccess", "profile": { "accessLevels": { "premium": { "id": "premium", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } } } } } // Pending purchase { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "purchaseResult": { "type": "AdaptyPurchaseResultPending" } } // User cancelled purchase { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "purchaseResult": { "type": "AdaptyPurchaseResultUserCancelled" } } ```
Chúng tôi khuyến nghị đóng màn hình trong trường hợp đó. Tham khảo [Phản hồi hành động nút bấm](flutter-handle-paywall-actions) để biết chi tiết về cách đóng màn hình paywall. #### Hoàn thành điều hướng thanh toán web \{#finished-web-payment-navigation\} Phương thức này được gọi sau khi thử mở một [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: ```javascript 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ụ sự kiện (Nhấn để mở rộng) ```javascript // Successful navigation { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "error": null } // Failed navigation { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "error": { "code": "web_navigation_failed", "message": "Failed to open web paywall", "details": { "underlyingError": "Browser unavailable" } } } ```
#### Mua thất bại \{#failed-purchase\} Phương thức này được gọi khi giao dịch mua thất bại (ví dụ: do lỗi thanh toán hoặc lỗi mạng). Phương thức này **không** kích hoạt khi người dùng chủ động hủy hoặc giao dịch đang chờ xử lý — những trường hợp đó được xử lý bởi `paywallViewDidFinishPurchase`: ```javascript showLineNumbers title="Flutter" void paywallViewDidFailPurchase(AdaptyUIPaywallView view, AdaptyPaywallProduct product, AdaptyError error) { } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```javascript { "product": { "vendorProductId": "premium_monthly", "localizedTitle": "Premium Monthly", "localizedDescription": "Premium subscription for 1 month", "localizedPrice": "$9.99", "price": 9.99, "currencyCode": "USD" }, "error": { "code": "purchase_failed", "message": "Purchase failed due to insufficient funds", "details": { "underlyingError": "Insufficient funds in account" } } } ```
#### Bắt đầu khôi phục \{#started-restore\} Nếu người dùng khởi tạo quá trình khôi phục, phương thức này sẽ được gọi: ```javascript showLineNumbers title="Flutter" void paywallViewDidStartRestore(AdaptyUIPaywallView view) { } ``` #### Khôi phục thành công \{#successful-restore\} Nếu khôi phục giao dịch mua thành công, phương thức này sẽ được gọi: ```javascript showLineNumbers title="Flutter" void paywallViewDidFinishRestore(AdaptyUIPaywallView view, AdaptyProfile profile) { } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```javascript { "profile": { "accessLevels": { "premium": { "id": "premium", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } }, "subscriptions": [ { "vendorProductId": "premium_monthly", "isActive": true, "expiresAt": "2024-02-15T10:30:00Z" } ] } } ```
Chúng tôi khuyến nghị đóng màn hình nếu người dùng có `accessLevel` yêu cầu. Tham khảo chủ đề [Trạng thái gói đăng ký](flutter-listen-subscription-changes) để tìm hiểu cách kiểm tra và chủ đề [Phản hồi hành động nút bấm](flutter-handle-paywall-actions) để tìm hiểu cách đóng màn hình paywall. #### Khôi phục thất bại \{#failed-restore\} Nếu khôi phục giao dịch mua thất bại, phương thức này sẽ được gọi: ```javascript showLineNumbers title="Flutter" void paywallViewDidFailRestore(AdaptyUIPaywallView view, AdaptyError error) { } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```javascript { "error": { "code": "restore_failed", "message": "Purchase restoration failed", "details": { "underlyingError": "No previous purchases found" } } } ```
### Tải dữ liệu và hiển thị \{#data-fetching-and-rendering\} #### Lỗi tải sản phẩm \{#product-loading-errors\} Nếu bạn không truyền mảng sản phẩm trong quá trình khởi tạo, AdaptyUI sẽ tự động lấy các đối tượng cần thiết từ 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 này: ```javascript showLineNumbers title="Flutter" void paywallViewDidFailLoadingProducts(AdaptyUIPaywallView view, AdaptyError error) { } ```
Ví dụ sự kiện (Nhấn để mở rộng) ```javascript { "error": { "code": "products_loading_failed", "message": "Failed to load products from the server", "details": { "underlyingError": "Network timeout" } } } ```
#### Lỗi hiển thị \{#rendering-errors\} Nếu xảy ra lỗi trong quá trình hiển thị giao diện, lỗi đó sẽ được báo cáo bằng cách gọi phương thức này. Theo mặc định (kể từ v3.15.2), paywall sẽ tự động bị đóng khi xảy ra lỗi hiển thị, nhưng bạn có thể ghi đè hành vi này nếu cần. ```javascript 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) ```javascript { "error": { "code": "rendering_failed", "message": "Failed to render paywall interface", "details": { "underlyingError": "Invalid paywall configuration" } } } ```
Trong điều kiện bình thường, các lỗi như vậ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 tệp cấu hình dự phòng vào thư mục `assets` của ứng dụng ở 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 mục tiêu. ```javascript showLineNumbers title="javascript" 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 tệp 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à mã locale trong Flutter SDK" description: "Quản lý localizations và mã locale 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\} Có một số tình huống mà mã locale phát huy tác dụng — ví dụ, khi bạn cần lấy paywall phù hợp với localization hiện tại của ứng dụng. Vì mã locale khá phức tạp và có thể khác nhau tùy nền tảng, chúng tôi sử dụng một tiêu chuẩn nội bộ cho tất cả các nền tảng được hỗ trợ. Tuy nhiên, chính vì độ phức tạp đó, bạn cần hiểu rõ mình đang gửi gì lên server để nhận đúng localization — và điều gì xảy ra tiếp theo — để luôn nhận được kết quả như mong đợi. ## Tiêu chuẩn mã locale tại Adapty \{#locale-code-standard-at-adapty\} Đối với mã locale, Adapty sử dụng phiên bản được điều chỉnh nhẹ của [tiêu chuẩn BCP 47](https://en.wikipedia.org/wiki/IETF_language_tag): mỗi mã 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ã locale \{#locale-code-matching\} Khi Adapty nhận được yêu cầu từ SDK phía client kèm mã locale và bắt đầu tìm localization tương ứng của paywall, quá trình diễn ra như sau: 1. Chuỗi locale đầu vào được chuyển thành chữ thường và tất cả dấu gạch dưới (`_`) được thay bằng dấu gạch ngang (`-`) 2. Hệ thống tìm kiếm localization có mã locale khớp hoàn toàn 3. Nếu không tìm thấy, hệ thống lấy phần trước dấu gạch ngang đầu tiên (`pt` từ `pt-br`) và tiếp tục tìm kiếm 4. Nếu vẫn không tìm thấy, hệ thống trả về localization mặc định `en` Nhờ vậy, một thiết bị iOS gửi `'pt_BR'`, một thiết bị Android gửi `pt-BR`, và một thiết bị khác gửi `pt-br` đều nhận được kết quả giống nhau. ## Triển khai localizations: cách khuyến nghị \{#implementing-localizations-recommended-way\} Nếu bạn đang quan tâm đến localizations, nhiều khả năng bạn đã làm việc với các file chuỗi đã được bản địa hóa trong dự án. Trong trường hợp đó, chúng tôi khuyến nghị thêm một cặp key-value chứa mã locale Adapty tương ứng vào mỗi file localization. Sau đó, lấy giá trị của key đó khi gọi SDK, như sau: ```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 ``` Cách này giúp bạn kiểm soát hoàn toàn localization nào sẽ được lấy về cho từng người dùng của ứng dụng. ## Triển khai localizations: cách khác \{#implementing-localizations-the-other-way\} Bạn cũng 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 mã locale một cách tường minh cho từng localization. Điều đó có nghĩa là lấy mã locale từ các đối tượng khác mà nền tảng 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 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 localization được chọn đúng, bạn phải dựa vào logic của Apple — vốn hoạt động tốt nếu bạn dùng cách khuyến nghị với file chuỗi đã được bản địa hóa — hoặc tự tái tạo logic đó. 2. Rất khó dự đoán chính xác server của Adapty sẽ nhận được gì. 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 đó bạn sẽ không nhận được localization `ar-om` như mong đợi, mà thay vào đó là `ar` — điều có thể gây bất ngờ. Nếu bạn vẫn quyết định dùng cách này, hãy đảm bảo đã xử lý tất cả các trường hợp liên quan. --- # 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 cần qua phí và kiểm duyệt của App Store." --- :::important Trước khi bắt đầu, hãy đảm bảo bạn đã [cấu hình web paywall trên dashboard](web-paywall) và cài đặt Adapty SDK phiên bản 3.6.1 trở lên. ::: 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ể được hiển thị cho người dùng với trang web mà họ được chuyển hướng đến. 2. Theo dõi khi người dùng quay lại ứng dụng, sau đó gọi `.getProfile` theo các khoảng thời gian 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ờ đó, 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. Sử dụng khi các sản phẩm trong Adapty paywall của bạn khác với những sản phẩm trong web paywall. ::: #### Xử lý lỗi \{#handle-errors\} | Lỗi | Mô tả | Hành động khuyến nghị | |-----------------------------------------|------------------------------------------------------------|------------------------------------------------------------------------------------| | AdaptyError.paywallWithoutPurchaseUrl | Paywall chưa được cấu hình URL mua hàng qua web | Kiểm tra xem paywall đã được cấu hình đúng trong Adapty Dashboard chưa | | AdaptyError.productWithoutPurchaseUrl | Sản phẩm chưa có URL mua hàng qua web | Xác minh cấu hình sản phẩm trong Adapty Dashboard | | AdaptyError.failedOpeningWebPaywallUrl | Không thể mở URL trong trình duyệt | Kiểm tra cài đặt thiết bị hoặc cung cấp phương thức mua hàng thay thế | | AdaptyError.failedDecodingWebPaywallUrl | Không thể mã hóa đúng các tham số trong URL | 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 mở trong trình duyệt bên ngoài. Để mang lại trải nghiệm liền mạch cho người dùng, bạn có thể mở web paywall trong trình duyệt trong ứng dụng. Cách này hiển thị trang mua hàng web ngay trong ứng dụng của bạn, cho phép người dùng hoàn tất giao dịch mà không cần chuyển sang ứng dụng khác. Để bật tính năng này, đặt tham số `in` thành `.inAppBrowser`: ```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 xử lý 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 thể truy xuất 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 bị tính gấp đôi \{#the-paywall-view-number-is-too-big\} **Vấn đề**: Số lượt xem paywall hiển thị gấp đôi so với dự kiến. **Nguyên nhân**: Bạn có thể đang gọi `logShowPaywall` trong code, khiến số lượt xem bị tính trùng khi sử dụng Paywall Builder. Với các paywall được thiết kế bằng Paywall Builder, 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 `logShowPaywall` trong code nếu đang sử dụng Paywall Builder. ## Các vấn đề khác \{#other-issues\} **Vấn đề**: Bạn đang gặp các sự cố liên quan đến Paywall Builder chưa được đề cập ở trên. **Giải pháp**: Nếu cần, hãy migrate SDK lên phiên bản mới nhất theo [hướng dẫn migration](flutter-sdk-migration-guides). Nhiều sự cố đã được khắc phục trong các phiên bản SDK mới hơn. --- # 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 để 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 Adapty SDK tự động lấy sản phẩm, xử lý giao dịch mua mới và khôi phục các giao dịch trước đó. :::important **Hướng dẫn này dành cho các nhà phát triển đang triển khai paywall tùy chỉnh.** Nếu bạn muốn cách đơn giản nhất để bậ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 trong trình chỉnh sửa trực quan không cần code, Adapty tự động xử lý toàn bộ logic mua hàng, và bạn có thể thử nghiệm các thiết kế khác nhau mà không cần phát hành lại ứng dụng. ::: ## Trước khi bắt đầu \{#before-you-start\} ### Thiết lập sản phẩm \{#set-up-products\} Để bật tính năng in-app purchase, bạn cần hiểu ba khái niệm chính: - [**Sản phẩm**](product) – bất cứ thứ gì người dùng có thể mua (gói đăng ký, consumable, quyền truy cập trọn đời) - [**Paywalls**](paywalls) – các cấu hình xác định sản phẩm nào sẽ được cung cấp. Trong Adapty, paywall là cách duy nhất để lấy sản phẩm, nhưng thiết kế này cho phép bạn thay đổi sản phẩm, giá cả và ưu đãi mà không cần chỉnh sửa code ứng dụng. - [**Placements**](placements) – nơi và thời điểm bạn hiển thị paywall trong ứng dụng (như `main`, `onboarding`, `settings`). Bạn thiết lập paywall cho placement trong 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 paywall khác nhau cho từng nhóm người dùng. Hãy đảm bảo bạn hiểu những khái niệm này ngay cả khi làm việc với paywall tùy chỉnh. Về cơ bản, đây chỉ là cách bạn quản lý các sản phẩm bán trong ứng dụng của mình. Để 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 sản phẩm của mình. Để hiểu những gì cần làm trên dashboard, hãy theo dõi hướng dẫn bắt đầu nhanh [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ần xác thực phía backend. Tuy nhiên, Adapty SDK xử lý người dùng ẩn danh và đã xác định theo cách khác nhau. Đọc [hướng dẫn bắt đầu nhanh về xác định người dùng](flutter-quickstart-identify) để hiểu rõ đặc điểm 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 sản phẩm \{#step-1-get-products\} Để lấy sản phẩm cho paywall tùy chỉnh, bạn cần: 1. Lấy đối tượng `paywall` bằng cách truyền [placement](placements) ID vào phương thức `getPaywall`. 2. Lấy mảng sản phẩm cho paywall này bằng phương thức `getPaywallProducts`. ```dart showLineNumbers Future loadPaywall() async { try { final paywall = await Adapty().getPaywall(placementId: 'YOUR_PLACEMENT_ID'); final products = await Adapty().getPaywallProducts(paywall: paywall); // Use products to build your custom paywall UI } on AdaptyError catch (adaptyError) { // Handle the error } catch (e) { // Handle the error } } ``` ## Bước 2. Chấp nhận thanh toán \{#step-2-accept-purchases\} Khi người dùng nhấn vào một sản phẩm trong paywall tùy chỉnh của bạn, hãy gọi phương thức `makePurchase` với sản phẩm đã chọn. Phương thức này sẽ xử lý luồng mua hàng 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. Phương thức này sẽ đồng bộ lịch sử mua hàng 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 } } ``` ## Các bước tiếp theo \{#next-steps\} --- no_index: true --- import Callout from '../../../components/Callout.astro'; Bạn có câu hỏi hoặc gặp sự cố? Hãy xem [diễn đàn hỗ trợ](https://adapty.featurebase.app/) của chúng tôi — nơi bạn có thể tìm câu trả lời cho các câu hỏi thường gặp hoặc đặt câu hỏi của riêng mình. Đội ngũ và cộng đồng của chúng tôi luôn sẵn sàng giúp đỡ! Paywall của bạn đã sẵn sàng để hiển thị trong ứng dụng. Kiểm tra giao dịch mua trong [sandbox của App Store](test-purchases-in-sandbox) hoặc trên [Google Play Store](testing-on-android) để đảm bảo bạn có thể hoàn thành một giao dịch mua thử từ paywall. Để xem cách hoạt động trong một triển khai sẵn sàng cho môi trường production, 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 cách xử lý giao dịch mua với error handling phù hợp, UI observers và tích hợp SDK toàn diện. Tiếp theo, [kiểm tra xem người dùng đã hoàn thành giao dịch mua chưa](flutter-check-subscription-status) để quyết định có nên hiển thị paywall hay cấp quyền truy cập vào các tính năng trả phí. --- # File: fetch-paywalls-and-products-flutter --- --- title: "Lấy paywalls và sản phẩm cho Remote Config paywalls trong Flutter SDK" description: "Lấy paywalls và sản phẩm trong Adapty Flutter SDK để tăng cường khả năng kiếm tiền từ người dùng." --- Trước khi hiển thị Remote Config và các paywall tùy chỉnh, bạn cần lấy thông tin về chúng. Lưu ý rằng chủ đề này đề cập đến Remote Config và các paywall tùy chỉnh. Để biết hướng dẫn lấy paywalls cho Paywall Builder, vui lòng tham khảo [Lấy Paywall Builder paywalls 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 paywalls và sản phẩm trong ứng dụng của bạn (click để mở rộng) 1. [Tạo sản phẩm](create-product) trong Adapty Dashboard. 2. [Tạo paywall và thêm sản phẩm vào paywall](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) trong ứng dụ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 paywalls, cho phép bạn hiển thị chúng trong các placement cụ thể của ứng dụng. Để hiển thị sản phẩm, bạn cần lấy một [Paywall](paywalls) từ một trong các [placement](placements) của bạn bằng phương thức `getPaywall`. :::important **Đừng hardcode ID sản phẩm.** ID duy nhất bạn nên hardcode là placement ID. Paywalls được cấu hình từ xa, vì vậy số lượng sản phẩm và các ư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 một 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(id: "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 được kỳ vọng là một mã ngôn ngữ gồm một hoặc nhiều subtag được phân cách bằng dấu trừ (**-**). Subtag đầu tiên dành cho ngôn ngữ, subtag thứ hai dành cho vùng.

Ví dụ: `en` nghĩa là tiếng Anh, `pt-br` đại diện cho 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 trong trường hợp thất bại. Chúng tôi khuyến nghị tùy chọn này vì nó đảm bảo người dùng 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 có 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 sẽ có trải nghiệm tải nhanh hơn, dù kết nối internet của họ có chập chờn thế nào. Cache được cập nhật thường xuyên, nên sử dụng trong phiên để 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 ứng dụng được cài đặt lại hoặc xóa thủ công.

Adapty SDK lưu trữ paywalls ở 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 để lấy paywalls 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 paywalls, đồng thời đảm bảo độ tin cậy ngay cả khi kết nối internet yếu.

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

Giá trị này giới hạn thời gian chờ cho phương thức này. Nếu hết thời gian chờ, dữ liệu đã 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.

| Đừng hardcode ID sản phẩm! Vì paywalls được cấu hình từ xa, các sản phẩm có sẵn, số lượng sản phẩm và các ưu đãi đặc biệt (như dùng thử miễn phí) có thể thay đổi theo thời gian. Hãy đảm bảo code của bạn xử lý được các tình huống này. Ví dụ, nếu ban đầu bạn lấy được 2 sản phẩm, ứng dụng nên hiển thị 2 sản phẩm đó. Tuy nhiên, nếu sau này bạn lấy được 3 sản phẩm, ứng dụng nên hiển thị cả 3 mà không cần thay đổi code. Thứ duy nhất bạn phải hardcode là placement ID. 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) gồm: danh sách ID sản phẩm, định danh paywall, Remote Config, và một số thuộc tính khác. | ## Lấy sản phẩm \{#fetch-products\} Sau khi có paywall, bạn có thể truy vấn mảng sản phẩm tương ứng với nó: ```dart showLineNumbers try { final products = await Adapty().getPaywallProducts(paywall: paywall); // the requested products array } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` 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) gồm: định danh sản phẩm, tên sản phẩm, giá, 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ế giao diện paywall, bạn thường cần truy cập các thuộc tính này 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 đủ thông tin về tất cả các thuộc tính có sẵn. | Thuộc tính | Mô tả | |-------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **Title** | Để hiển thị tiêu đề sản phẩm, dùng `product.localizedTitle`. Lưu ý rằng bản địa hóa dựa trên quốc gia cửa hàng mà người dùng đã chọn, chứ không phải locale của thiết bị. | | **Price** | Để hiển thị giá đã được bản địa hóa, dùng `product.price.localizedString`. Bản địa hóa này dựa trên thông tin locale 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, dùng `product.price.currencySymbol`. | | **Subscription Period** | Để hiển thị chu kỳ (ví dụ: tuần, tháng, năm, v.v.), dùng `product.subscription?.localizedPeriod`. Bản địa hóa này dựa trên locale của thiết bị. Để lấy chu kỳ gói đăng ký theo chương trình, 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ụ, đối 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. | | **Introductory Offer** | Để hiển thị huy hiệu hoặc chỉ báo khác rằng gói đăng ký có ưu đãi giới thiệu, hãy xem thuộc tính `product.subscription?.offer?.phases`. Đây là một 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. 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`. Dùng thử miễn phí sẽ thuộc loại `AdaptyPaymentMode.freeTrial`.
• `price`: Giá giảm dưới dạng số. Đối với dùng thử miễn phí, tìm giá trị `0` ở đây.
• `localizedNumberOfPeriods`: một chuỗi được bản địa hóa theo locale 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 riêng lẻ về chu kỳ ưu đãi bằng thuộc tính này. Nó hoạt động theo cách tương tự cho các ưu đãi như phần trước đã mô tả.
• `localizedSubscriptionPeriod`: Chu kỳ gói đăng ký được định dạng theo locale của người dùng cho phần giảm giá. | ## Tăng tốc lấy paywall với paywall đối tượng mặc định \{#speed-up-paywall-fetching-with-default-audience-paywall\} Thông thường, paywalls được lấy 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à paywalls, và người dùng của bạn có kết nối internet yếu, việc lấy 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. Để 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 lấy 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 paywall bằng phương thức `getPaywall`, như được mô tả 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ị dùng `getPaywall` Phương thức `getPaywallForDefaultAudience` có một số hạn chế đáng kể: - **Có thể gặp vấn đề tương thích ngược**: Nếu bạn cần hiển thị các paywall khác nhau cho các phiên bản ứng dụng khác nhau (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ế paywalls 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 paywalls không được render. - **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 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 bạn). Nếu bạn sẵn sàng chấp nhận những hạn chế này để hưởng lợi từ việc lấy paywall nhanh hơn, hãy sử dụng phương thức `getPaywallForDefaultAudience` như mô tả dưới đây. Nếu không, hãy tiếp tục dùng `getPaywall` như đã mô tả [ở trên](fetch-paywalls-and-products-flutter#fetch-paywall-information). ::: :::note Phương thức `getPaywallForDefaultAudience` chưa được hỗ trợ trong Flutter SDK, nhưng sẽ sớm được thêm vào. ::: | 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 được kỳ vọng là một mã ngôn ngữ gồm một hoặc nhiều subtag được phân cách bằng dấu trừ (**-**). Subtag đầu tiên dành cho ngôn ngữ, subtag thứ hai dành cho vùng.

Ví dụ: `en` nghĩa là tiếng Anh, `pt-br` đại diện cho 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 trong trường hợp thất bại. Chúng tôi khuyến nghị tùy chọn này vì nó đảm bảo người dùng 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 có 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 sẽ có trải nghiệm tải nhanh hơn, dù kết nối internet của họ có chập chờn thế nào. Cache được cập nhật thường xuyên, nên sử dụng trong phiên để 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 ứng dụng được cài đặt lại hoặc xóa thủ công.

| --- # File: present-remote-config-paywalls-flutter --- --- title: "Render paywall designed by remote config in 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 render trong code của ứng dụng để hiển thị nó cho người dùng. Vì Remote Config linh hoạt theo nhu cầu của bạn, bạn hoàn toàn chủ động quyết định nội dung 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 tự do 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ị nó \{#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(id: "YOUR_PLACEMENT_ID"); final String? headerText = paywall.remoteConfig?.dictionary?['header_text'] as String?; } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Sau khi đã nhận được tất cả các giá trị cần thiết, đã đến lúc render và ghép chúng thành một trang trực quan hấp dẫn. Hãy đảm bảo thiết kế tương thích với nhiều kích thước màn hình và hướng 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](present-remote-config-paywalls-flutter#track-paywall-view-events) như mô tả bên dưới, để Adapty analytics có thể thu thập dữ liệu cho funnel và A/B test. ::: Sau khi hiển thị paywall xong, tiếp tục thiết lập flow thanh toán. 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. Xem chi tiết về phương thức `.makePurchase()` tại [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 gọi là fallback paywall](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 dữ liệu mua hàng được thu thập tự độ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 khách hà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 funnel và A/B test. :::important Không cần gọi `.logShowPaywall(paywall)` nếu bạn đang hiển thị paywall được tạo trong [Paywall Builder](adapty-paywall-builder). ::: ```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** | bắt buộc | 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ỉ cần hiển thị paywall là đủ để hỗ trợ mua hàng nếu bạn sử dụng [Paywall Builder](adapty-paywall-builder) để tùy chỉnh paywall. Nếu bạn không sử dụng Paywall Builder, bạn cần dùng một phương thức riêng 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 paywall và tiến hành các giao dịch họ muốn. Nếu paywall của bạn có ưu đãi đang hoạt động cho sản phẩm mà người dùng đang cố mua, Adapty sẽ tự động áp dụng ưu đãi đó tại thời điểm mua hàng. :::warning Lưu ý rằng ưu đãi giới thiệu sẽ chỉ được áp dụng tự động nếu bạn sử dụng paywall được thiết lập bằng Paywall Builder. Trong các trường hợp khác, bạn cần [xác minh đ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 bị từ chối khi phát hành. Hơn nữa, điều này có thể dẫn đến việc tính giá đầy đủ cho những người dùng đủ điều kiện nhận ưu đãi giới thiệu. ::: Hãy đảm bảo bạn đã [hoàn thành 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. **Muốn có 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 | Đố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 toàn diện về các mức độ truy cập, gói đăng ký và sản phẩm mua một lần của người dùng trong ứng dụng.

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

| :::warning **Lưu ý:** nếu bạn vẫn đang dùng phiên bản StoreKit của Apple thấp hơn v2.0 và phiên bản Adapty SDK 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 deprecated. ::: ## 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 phụ thuộc vào cửa hàng: - Đối với App Store, gói đăng ký được cập nhật tự động trong nhóm đă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 từ nhóm khác, cả hai gói đăng ký sẽ hoạt động đồng thời. - Đối với Google Play, gói đăng ký không được cập nhật tự động. Bạn cần quản lý việc chuyển đổi trong mã nguồn ứng dụng di độ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 result = await adapty.makePurchase( product: product, 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: | Tham số | Bắt buộc | Mô tả | | :--------------------------- | :------- |:--------------------------------------------------------------------------------------------------------| | **subscriptionUpdateParams** | bắt buộc | Đố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 Google Developer: - [Về các chế độ thay thế](https://developer.android.com/google/play/billing/subscriptions#replacement-modes) - [Khuyến nghị 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ỉ khả 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 ý: Thay đổi gói đăng ký thực sự sẽ chỉ xảy ra khi chu kỳ thanh toán của gói đăng ký hiện tại kết thúc. ## Đổi mã ưu đãi trên iOS \{#redeem-offer-codes-in-ios\} --- no_index: true --- import Callout from '../../../components/Callout.astro';
Về offer code Offer code cho phép bạn tặng ưu đãi hoặc dùng thử miễn phí cho những người dùng cụ thể. Không giống như các ưu đãi thông thường được áp dụng tự động, offer code được phân phối bên ngoài ứng dụng — qua email marketing, mạng xã hội, hoặc tài liệu in ấn. Người dùng đổi code bằng cách nhập vào App Store, truy cập URL đổi thưởng, hoặc qua hộp thoại trong ứng dụng. Để thiết lập offer code, mở một gói đăng ký trong App Store Connect và vào mục **Offer Codes**. Bạn có thể tạo [ba loại](https://developer.apple.com/help/app-store-connect/manage-subscriptions/set-up-subscription-offer-codes) offer code: - **Free** — gói đăng ký miễn phí trong một khoảng thời gian nhất định, sau đó gia hạn với giá đầy đủ. - **Pay as you go** — người dùng trả giá ưu đãi theo từng chu kỳ thanh toán trong một khoảng thời gian, sau đó gói đăng ký gia hạn với giá đầy đủ. - **Pay up front** — người dùng trả một lần với giá ưu đãi cho toàn bộ thời gian ưu đãi, sau đó gói đăng ký gia hạn với giá đầy đủ. Bạn không cần thêm offer code vào Adapty. Apple gắn tag mọi giao dịch trong thời gian ưu đãi với danh mục offer code. Điều này bao gồm lần đổi code đầu tiên và tất cả các lần gia hạn ưu đãi tiếp theo. Adapty phát hiện tag đó và ghi lại từng giao dịch với danh mục ưu đãi `offer_code`. Khi thời gian ưu đãi kết thúc và gói đăng ký gia hạn với giá đầy đủ, tag đó sẽ không còn nữa. Bạn có thể lọc analytics theo loại ưu đãi **Offer Code** trên [Adapty Dashboard](controls-filters-grouping-compare-proceeds). #### Xử lý sự chênh lệch doanh thu \{#revenue-discrepancy-troubleshooting\} Nếu bạn nhận thấy một giao dịch offer code xuất hiện trong Adapty với giá đầy đủ thay vì giá ưu đãi, hãy kiểm tra lại những điểm sau trong App Store Connect: - Offer code đã được cấu hình đúng giá cho tất cả các khu vực mà người dùng có thể đổi code. - Giá ưu đãi đã được thiết lập cho quốc gia hoặc khu vực cụ thể của người dùng. Apple gửi giá theo khu vực trong giao dịch. Nếu không có giá khu vực nào được cấu hình cho ưu đãi, Apple có thể gửi giá đầy đủ của sản phẩm. Bạn có thể lọc và kiểm tra các giao dịch offer code trên [Adapty Dashboard](controls-filters-grouping-compare-proceeds) theo loại ưu đãi **Offer Code** và bộ lọc **Offer Discount Type**. #### Promo code cũ (đã ngừng hỗ trợ) \{#legacy-promo-codes-deprecated\} Apple đã ngừng hỗ trợ promo code cho in-app purchase vào tháng 3 năm 2026. Offer code thay thế chúng với nhiều tính năng hơn: điều kiện tham gia có thể cấu hình, ngày hết hạn, và lên đến 1 triệu code mỗi quý. Nếu trước đây bạn sử dụng promo code cho in-app purchase, hãy chuyển sang offer code trong App Store Connect. Promo code cũ (giới hạn 100 code mỗi ứng dụng mỗi phiên bản) cấp quyền truy cập miễn phí vào một gói đăng ký. Không giống như offer code, Apple không đưa thông tin giảm giá vào giao dịch promo code — Apple gửi giá đầy đủ của sản phẩm trong biên lai. Kết quả là Adapty ghi lại các giao dịch này theo giá đầy đủ, gây ra sự chênh lệch doanh thu giữa Adapty analytics và App Store Connect. Nếu bạn thấy các giao dịch lịch sử với giá đầy đủ trong khi đáng lẽ phải miễn phí, nhiều khả năng đó là từ promo code cũ. Vì các code này đã bị ngừng hỗ trợ, hãy chuyển sang offer code để theo dõi doanh thu chính xác.
Để hiển thị trang đổi mã trong ứng dụng của bạn: ```dart showLineNumbers try { await Adapty().presentCodeRedemptionSheet(); } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { // handle the error } ``` :::danger Dựa trên quan sát của chúng tôi, trang Offer Code Redemption trong một số ứng dụng có thể không hoạt độ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ư 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 đã gỡ và cài lại ứng dụng, hoặc chuyển sang thiết bị mới và muốn tiếp tục sử dụng nội dung đã mua mà không cần 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 sẽ được khôi phục tự động mà không cần thêm code từ phía bạn. Nếu đó là trường hợp của bạn — bạn có thể bỏ qua bước này. ::: Để khôi phục giao dịch mua khi bạn không sử dụng [Paywall Builder](adapty-paywall-builder) để tùy chỉnh paywall, hãy gọi phương thức `.restorePurchases()`: ```javascript 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) { } ``` Các tham số trả về: | 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 chế độ Observer trong Flutter SDK" description: "Triển khai chế độ observer 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ý mua hàng riêng và chưa sẵn sàng chuyển hoàn toàn sang Adapty, bạn có thể khám phá [chế độ Observer](observer-vs-full-mode). Ở dạng cơ bản, chế độ Observer cung cấp tính năng 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à analytics. Nếu đây là những gì bạn cần, bạn chỉ cần: 1. Bật chế độ 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 có 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ý việc mua hàng và trạng thái gói đăng ký, đồng thời sử dụng Adapty để gửi các sự kiện gói đăng ký và analytics. :::important Khi chạy ở 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 đó. ::: ```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 chế độ Observer \{#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 thiết lập trong chế độ Observer. Đây là những gì bạn cần làm ngoài các bước đã nêu ở trên: 1. Hiển thị paywall như bình thường đối với [paywall dùng Remote Config](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. --- # File: report-transactions-observer-mode-flutter --- --- title: "Báo cáo giao dịch trong Observer Mode với 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 với Flutter SDK." --- Trong Observer Mode, Adapty SDK không thể tự động theo dõi các giao dịch mua hàng thực hiện qua hệ thống 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 của mình. Đ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 analytics. Sử dụng `reportTransaction` để báo cáo rõ ràng từng giao dịch để 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 bao gồm `variationId` khi báo cáo giao dịch. Điều này liên kết giao dịch mua hàng với paywall đã kích hoạt nó, đảm bảo analytics paywall chính xác. ```javascript 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ố: | Tham số | Bắt buộc | Mô tả | | ------------- | -------- | ------------------------------------------------------------ | | transactionId | bắt buộc |
  • Cho iOS: Mã định danh của giao dịch.
  • Cho Android: Mã định danh dạng chuỗi `purchase.getOrderId` của giao dịch mua hàng, trong đó purchase là một instance của lớp [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) trong thư viện billing.
| | variationId | tùy chọn | Mã định danh dạng chuỗi của biến thể. Bạn có thể lấy nó bằng thuộc tính `variationId` của đối tượng [AdaptyPaywall](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyPaywall-class.html). |
Trong Observer Mode, Adapty SDK không thể tự động theo dõi các giao dịch mua hàng thực hiện qua hệ thống 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 sai sót trong analytics. Sử dụng `reportTransaction` trên cả hai nền tảng để báo cáo rõ ràng từng giao dịch, và sử dụng `restorePurchases` trên Android như một bước bổ sung để đảm bảo Adapty nhận biết giao dịch đó. :::warning **Đừng bỏ qua việc báo cáo giao dịch và khôi phục giao dịch 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, 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 bao gồm `variationId` khi báo cáo giao dịch. Điều này liên kết giao dịch mua hàng với paywall đã kích hoạt nó, đảm bảo analytics paywall chính xác. ```javascript 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 |
  • Cho iOS, StoreKit 1: một đối tượng [SKPaymentTransaction](https://developer.apple.com/documentation/storekit/skpaymenttransaction).
  • Cho iOS, StoreKit 2: đối tượng [Transaction](https://developer.apple.com/documentation/storekit/transaction).
  • Cho Android: Mã định danh dạng chuỗi (purchase.getOrderId của giao dịch mua hàng, trong đó purchase là một instance của lớp [Purchase](https://developer.android.com/reference/com/android/billingclient/api/Purchase) trong thư viện billing.
| | variationId | tùy chọn | Mã định danh dạng chuỗi của biến thể. Bạn có thể lấy nó bằng thuộc tính `variationId` của đối tượng [AdaptyPaywall](https://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 trong 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** Sử 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 hàng trong Mobile Code](flutter-restore-purchase). :::warning **Đừng bỏ qua việ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** Adapty SDK không thể xác định nguồn gốc của các giao dịch mua hàng, vì bạn là người xử lý chúng. Do đó, nếu bạn định sử dụng paywall và/hoặc A/B test trong Observer Mode, bạn cần liên kết giao dịch đến từ cửa hàng ứng dụng của mình với paywall tương ứng trong code ứng dụng của bạn. Điều quan trọng là phải làm đúng trước khi phát hành ứng dụng, nếu không sẽ dẫn đến sai sót trong analytics. ```javascript 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\} --- no_index: true --- Khi SDK được kích hoạt, nó tự động đọc các quyền hiện có của người dùng từ StoreKit (iOS) hoặc Google Play Billing (Android) và đồng bộ chúng với backend của Adapty. Một gói đăng ký đang hoạt động sẽ xuất hiện trên hồ sơ người dùng Adapty mà không cần ứng dụng gọi `restorePurchases`. Điều **không** xảy ra tự động là nhận diện rằng một hồ sơ người dùng trên thiết bị mới thuộc về cùng một người dùng như hồ sơ trên thiết bị ban đầu. Adapty khớp các hồ sơ người dùng theo Customer User ID, vì vậy tính liên tục danh tính phụ thuộc vào những gì bạn sử dụng làm CUID. **Những gì Adapty có thể phát hiện trên nhiều thiết bị** | Cài đặt của bạn | Adapty phát hiện được gì | Bạn phải làm gì | | --- | --- | --- | | Customer User ID = `device_id` (không đăng nhập ứng dụng) | Thiết bị mới nhận được CUID khác và do đó có hồ sơ người dùng khác. Gói đăng ký đồng bộ với hồ sơ người dùng mới thông qua sự kiện **Access level updated**, nhưng `subscription_started` không kích hoạt — hồ sơ người dùng mới được coi là người kế thừa của giao dịch mua ban đầu. Các phân tích dựa trên `subscription_started` sẽ đếm thiếu những người dùng quay lại. | Sử dụng ID tài khoản ổn định làm Customer User ID để người dùng quay lại khớp với hồ sơ người dùng hiện có trên các thiết bị. | | Customer User ID = ID tài khoản ổn định (đăng nhập trên mọi thiết bị) | SDK tự động đồng bộ gói đăng ký khi `activate()`, và `identify()` khớp hồ sơ người dùng hiện có theo CUID. | Không cần cài đặt thêm — cả danh tính lẫn gói đăng ký đều được xử lý tự động. | | Người kế thừa Apple Family Sharing | Thành viên gia đình nhận gói đăng ký thông qua sự kiện **Access level updated** — `subscription_started` không kích hoạt. | Lắng nghe sự kiện **Access level updated**. Xem [Apple Family Sharing](apple-family-sharing) để biết ma trận sự kiện đầy đủ. | | Cùng tài khoản Apple/Google, người dùng khác nhau trong ứng dụng | Hồ sơ người dùng đầu tiên ghi lại giao dịch mua trở thành hồ sơ cha. Các hồ sơ người dùng tiếp theo thấy gói đăng ký thông qua chuỗi kế thừa, với một sự kiện **Access level updated**. | Yêu cầu đăng nhập, sau đó chọn [chế độ chia sẻ](sharing-paid-access-between-user-accounts) phù hợp với mô hình của bạn. | **Khôi phục giao dịch mua trên thiết bị mới** Hiển thị nút "Khôi phục giao dịch mua" do người dùng khởi tạo trên paywall của bạn. Apple App Review (hướng dẫn 3.1.1) yêu cầu có nút này, và nó đóng vai trò dự phòng khi quá trình đồng bộ tự động bỏ sót một trường hợp đặc biệt. Nút này nên gọi `restorePurchases` trên SDK của bạn. Không cần gọi `restorePurchases` theo chương trình khi khởi chạy lần đầu trong điều kiện sử dụng bình thường — SDK đã thực hiện tương đương khi `activate()`. Chỉ dùng các lệnh gọi theo chương trình để buộc kiểm tra biên lai mới, ví dụ khi debug trường hợp mất quyền truy cập sau khi `activate()` đã hoàn thành. --- # File: 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. ### Thiết lập thuộc tính người dùng \{#setting-user-attributes\} Để thiết lập thuộc tính người dùng, 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) { } ``` Lưu ý rằng các thuộc tính bạn đã thiết lập trước đó bằng phương thức `updateProfile` sẽ không bị xóa. :::tip Muốn xem ví dụ thực tế về cách tích hợp Adapty SDK vào ứng dụng di động? Hãy xem [ứng dụng mẫu](sample-apps) của chúng tôi, nơi minh họa toàn bộ quá trình thiết lập, bao gồm hiển thị paywall, thực hiện mua hàng và các chức năng cơ bản khác. ::: ### Danh sách các 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 tùy chỉnh của người dùng \{#custom-user-attributes\} Bạn có thể thiết lập các thuộc tính tùy chỉnh của riêng mình. Những thuộc tính này thường liên quan đến cách người dùng sử dụng ứng dụng. Ví dụ, với ứng dụng thể dục, đó có thể là số buổi tập mỗi tuần; với ứng dụng học ngoại ngữ, đó là trình độ của người dùng, v.v. Bạn có thể dùng chúng trong các phân khúc để tạo paywall và ưu đãi có mục tiêu, đồng thời dùng trong phân tích để xác định chỉ số sản phẩm nào ảnh hưởng nhiều nhất đến doanh thu. ```javascript 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 một key hiện có, dùng phương thức `.withRemoved(customAttributeForKey:)`: ```javascript 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 thiết lập trước đó. Để làm điều này, hãy dùng trường `customAttributes` của đối tượng `AdaptyProfile`. :::warning Lưu ý rằng giá trị của `customAttributes` có thể không cập nhật kịp thời, vì thuộc tính người dùng có thể được gửi từ nhiều thiết bị khác nhau bất kỳ lúc nào, do đó các thuộc tính trên server có thể đã thay đổi kể từ 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 đăng ký trong Flutter SDK" description: "Theo dõi và quản lý trạng thái gói đăng ký của người dùng trong Adapty để cải thiện khả năng giữ chân khách hàng 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 dễ dàng hơn bao giờ hết. Bạn không cần phải chèn thủ công ID sản phẩm 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 đă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à 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. Nhờ vậy, bạn có thể dùng đối tượng hồ sơ người dùng mà không cần gọi lại nhiều lần. Để nhận thông báo khi hồ sơ người dùng thay đổi, hãy lắng nghe sự kiện cập nhật hồ sơ như 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ừ server \{#retrieving-the-access-level-from-the-server\} Để lấy mức độ truy cập từ server, dùng phương thức `.getProfile()`: ```javascript showLineNumbers try { final profile = await Adapty().getProfile(); // check the access } on AdaptyError catch (adaptyError) { // handle the error } catch (e) { } ``` Tham số trả về: | 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 sẽ trả về kết quả mới nhất. Nếu vì lý do nào đó (ví dụ: không có kết nối internet), Adapty SDK không thể lấy thông tin từ server, dữ liệu từ cache sẽ được trả về thay thế. Ngoài ra, Adapty SDK cũng thường xuyên cập nhật cache của `AdaptyProfile` để đảm bảo thông tin luôn ở trạng thái mới nhất.

| Phương thức `.getProfile()` cung cấp hồ sơ người dùng, từ đó bạn có thể kiểm tra trạng thái mức độ truy cập. Một ứng dụng có thể có nhiều mức độ truy cập. Chẳng hạn, nếu bạn có ứng dụng đọc báo và bán gói đăng ký theo từng chủ đề riêng biệt, bạn có thể tạo các mức độ truy cập "sports" và "science". Tuy nhiên, trong hầu hết trường hợp, bạn chỉ cần một mức độ truy cập duy nhất — khi đó, hãy 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": ```javascript 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: ```javascript 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. ### Cache trạng thái đăng ký \{#subscription-status-cache\} Cache được tích hợp trong Adapty SDK lưu trữ trạng thái gói đăng ký của hồ sơ người dùng. Điều này có nghĩa là ngay cả khi server không khả dụng, dữ liệu cache 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ể yêu cầu dữ liệu trực tiếp từ cache. SDK định kỳ truy vấn server mỗi phút để kiểm tra có cập nhật hay thay đổi nào liên quan đến hồ sơ người dùng khô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 đồng bộ vào dữ liệu cache để giữ cho cache luôn nhất quán với server. --- # 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. ## Cần làm 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, hãy cẩn thận khi sử dụng customer user ID. ID người dùng có định dạng `` sẽ bị coi là thu thập dữ liệu cá nhân, tương tự như sử dụng email. Với Chế độ Trẻ em, cách làm tốt nhất là dùng các định danh ngẫu nhiên hoặc ẩn danh (ví dụ: ID đã băm hoặc UUID do thiết bị tạo) để đảm bảo tuân thủ. ## 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 code ứng dụng di động \{#updates-in-your-mobile-app-code\} Để tuân thủ chính sách, hãy tắt việc thu thập IDFA của người dùng (cho iOS), GAID/AAID (cho Android) và địa chỉ IP. **Android: Cập nhật cấu hình SDK** ```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 bằng CocoaPods** 1. Cập nhật Podfile của bạn: - Nếu bạn **chưa có** phần `post_install`, hãy thêm toàn bộ khối code bên dưới. - Nếu bạn **đã có** phần `post_install`, hãy hợp nhất các dòng được đánh dấu vào đó. ```ruby showLineNumbers title="Podfile" def adapty_enable_kids_mode(installer) installer.pods_project.targets.each do |target| next unless target.name == 'Adapty' target.build_configurations.each do |config| flags = config.build_settings['OTHER_SWIFT_FLAGS'] || '$(inherited)' flags = flags.join(' ') if flags.is_a?(Array) config.build_settings['OTHER_SWIFT_FLAGS'] = "#{flags} -DADAPTY_KIDS_MODE" end target.frameworks_build_phase.files.dup.each do |bf| target.frameworks_build_phase.remove_build_file(bf) if bf.display_name.to_s.include?('AdSupport') end end installer.pods_project.save Dir.glob(File.join(installer.sandbox.root, 'Target Support Files', '**', '*.xcconfig')).each do |xc| File.write(xc, File.read(xc).gsub(/\s*-framework\s+"?AdSupport"?/, '')) end end post_install do |installer| # ... keep your existing post_install body (Flutter adds one automatically) ... adapty_enable_kids_mode(installer) # <-- enable Adapty Kids Mode end ``` 2. Á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." --- Sau khi [bạn đã thiết kế phần giao diện cho onboarding](design-onboarding) bằng builder trên 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 cùng cấu hình hiển thị 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 onboarding \{#fetch-onboarding\} Khi bạn tạo một [onboarding](onboardings) bằng no-code builder của chúng tôi, nó được lưu dưới dạng một container chứa cấu hình mà ứng dụng của bạn cần lấy và hiển thị. Container này quản lý toàn bộ trải nghiệm — nội dung nào xuất hiện, cách trình bày, và cách xử lý các tương tác của người dùng (như câu trả lời quiz hoặc dữ liệu nhập form). Container cũng tự động theo dõi các sự kiện analytics, nên bạn không cần tự triển khai việc theo dõi lượt xem riêng. Để đạt hiệu suất tốt nhất, hãy lấy cấu hình onboarding sớm để ảnh có đủ thời gian tải xuống trước khi hiển thị cho người dùng. Để lấy onboarding, sử 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 } ``` Tiếp theo, 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ể dùng một lần. Nếu bạn cần dùng lại, hãy gọi lại phương thức `createOnboardingView`. Gọi nó 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 của bản địa hóa onboarding. Tham số này là một mã ngôn ngữ gồm một hoặc hai thẻ con phân tách bằng dấu gạch ngang (**-**). Thẻ con đầu tiên là ngôn ngữ, thẻ con thứ hai là vùng.

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

| | **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 khi gặp lỗi. Chúng tôi khuyến nghị tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.

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

Lưu ý rằng cache vẫn giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt hoặc xóa 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 dùng CDN để tải onboarding nhanh hơn và một máy chủ dự phòng độc lập trong trường hợp CDN không khả dụng. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản mới nhất của onboarding trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet kém.

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

Giá trị này giới hạn 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 `loadTimeout` đã chỉ định, vì thao tác có thể bao gồm nhiều yêu cầu bên dưới.

| Tham số phản hồi: | Tham số | Mô tả | |:----------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------| | Onboarding | Một đối tượng [`AdaptyOnboarding`](https://pub.dev/documentation/adapty_flutter/latest/adapty_flutter/AdaptyOnboarding-class.html) bao gồm: định danh và cấu hình của onboarding, Remote Config, và một số thuộc tính khác. | ## Tăng tốc lấy onboarding với onboarding đối tượng mặc định \{#speed-up-onboarding-fetching-with-default-audience-onboarding\} Thông thường, onboarding được lấy gần như ngay lập tức, 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 các trường hợp có nhiều đối tượng và onboarding, và người dùng của bạn có kết nối internet yếu, việc lấy onboarding có thể mất nhiều thời gian hơn mong muốn. Trong những tình huống như vậy, bạn có thể muốn hiển thị onboarding mặc định để đảm bảo trải nghiệm người dùng mượt mà thay vì không hiển thị onboarding nào. Để giải quyết điều này, bạn có thể dùng phương thức `getOnboardingForDefaultAudience`, phương thức này lấy onboarding của placement được chỉ định cho đối tượng **All Users**. Tuy nhiên, điều quan trọng cần hiểu là cách tiếp cận được khuyến nghị vẫn là lấy onboarding bằng phương thức `getOnboarding`, như mô tả chi tiết trong phần [Lấy 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, yêu cầu 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 "All Users", loại bỏ việc nhắm mục tiêu theo quốc gia, attribution, hoặc thuộc tính tùy chỉnh. Nếu tốc độ lấy nhanh hơn vượt trội hơn những hạn chế này trong trường hợp sử dụng của bạn, hãy dùng `getOnboardingForDefaultAudience` như 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 là một mã ngôn ngữ gồm một hoặc hai thẻ con phân tách bằng dấu gạch ngang (**-**). Thẻ con đầu tiên là ngôn ngữ, thẻ con thứ hai là vùng.

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

| | **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 khi gặp lỗi. Chúng tôi khuyến nghị tùy chọn này vì nó đảm bảo người dùng luôn nhận được dữ liệu mới nhất.

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

Lưu ý rằng cache vẫn giữ nguyên khi khởi động lại ứng dụng và chỉ bị xóa khi gỡ cài đặt hoặc xóa 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 dùng CDN để tải onboarding nhanh hơn và một máy chủ dự phòng độc lập trong trường hợp CDN không khả dụng. Hệ thống này được thiết kế để đảm bảo bạn luôn nhận được phiên bản mới nhất của onboarding trong khi vẫn đảm bảo độ tin cậy ngay cả khi kết nối internet kém.

| --- # File: 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." --- Nếu bạn đã tùy chỉnh một onboarding bằng builder, bạn không cần lo lắng về việc render nó trong code Flutter của mình để hiển thị cho người dùng. Onboarding đó đã bao gồm cả nội dung cần hiển thị lẫn cách thức 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) 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). 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ị 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ị lại onboarding, hãy gọi `createOnboardingView` thêm một lần nữa để tạo một instance `onboardingView` mới. :::warning 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`. ::: ```javascript 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 onboarding vào cây widget hiện có, sử dụng widget `AdaptyUIOnboardingPlatformView` trực tiếp trong cây widget Flutter của bạn. ```javascript 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, 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) } } ``` ::: ## Loader trong quá trình tải onboarding \{#loader-during-onboarding\} Khi hiển thị onboarding, bạn có thể nhận thấy một màn hình tải ngắn giữa splash screen 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 splash screen 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 màn hình hiển thị độc lập. ::: Cách tiếp cận đa nền tảng được khuyến nghị là giữ splash screen hoặc overlay tùy chỉnh hiển thị cho đến khi onboarding được tải đầy đủ, sau đó ẩn nó thủ công. Khi sử dụng widget nhúng, hãy đặt widget của bạn phủ lên trên và ẩn overlay 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ù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 mở trong trình duyệt trong ứng dụng. Điều này mang lại trải nghiệm liền mạch bằng cách hiển thị trang web ngay trong ứng dụng của bạn, cho phép người dùng xem mà 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 bên ngoài thay thế, bạn có thể tùy chỉnh hành vi này bằng cách đặt tham số `externalUrlsPresentation` thành `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 padding (Android) \{#disable-safe-area-paddings-android\} Theo mặc định, trên thiết bị Android, view onboarding tự động áp dụng safe area padding để tránh các thành phần UI 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 toàn bộ layout, bạn có thể thực hiện bằng cách thêm một boolean resource vào ứng dụng: 1. 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ả 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 sử dụng Adapty." --- Các onboarding được cấu hình bằng builder sẽ tạo ra các sự kiện mà ứng dụng của bạn có thể xử lý. 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 sử dụng: - **Hiển thị toàn màn hình**: Yêu cầu thiết lập một global event observer để xử lý 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 inline 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ị: ```javascript 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: ```javascript 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 widget nhúng \{#embedded-widget-events\} Khi sử dụng `AdaptyUIOnboardingPlatformView`, bạn có thể xử lý sự kiện thông qua các tham số callback inline trực tiếp trong widget. Lưu ý rằng sự kiện sẽ được gửi đến cả callback của widget và global observer (nếu đã thiết lập), nhưng global observer là tùy chọn: ```javascript 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ý custom action \{#handle-custom-actions\} Trong builder, bạn có thể thêm hành động **custom** vào một nút và gán ID cho nó. 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 như **Login** hoặc **Allow notifications**, phương thức delegate `onboardingController` sẽ được kích hoạt với case `.custom(id:)` và tham số `actionId` chính là **Action ID** từ builder. Bạn có thể tạo ID riêng của mình, như "allowNotifications". ```javascript // 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 thành tải onboarding \{#finishing-loading-onboarding\} Khi onboarding hoàn tất việc tải, sự kiện này sẽ được kích hoạt: ```javascript 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 nút có hành động **Close** được gán. :::important Lưu ý rằng bạn cần tự quản 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 đó. ::: ```javascript 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ở paywall \{#opening-a-paywall\} :::tip Xử lý sự kiện này để mở paywall nếu bạn muốn mở nó bên trong onboarding. Nếu bạn muốn mở paywall sau khi onboarding đóng, có một cách đơn giản hơn — xử lý sự kiện đó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, đối với iOS, chỉ một view (paywall hoặc onboarding) có thể được hiển thị trên màn hình cùng một lúc. Nếu bạn hiển thị paywall lên trên 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 dismiss onboarding sẽ đóng paywall thay vào đó, khiến onboarding vẫn còn hiển thị. Để tránh điều này, hãy luôn dismiss onboarding view trước khi hiển thị paywall. ```javascript 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 analytics event khi có các sự kiện liên quan đến điều hướng xảy ra trong flow onboarding: ```javascript 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 loại 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](design-onboarding). | | `unknown` | Dành cho bất kỳ loại sự kiện không được nhận diện nào. Bao gồm `name` (tên của sự kiện không xác định) và `meta` (metadata bổ sung) | Mỗi sự kiện bao gồm thông tin `meta` chứa: | Trường | Mô tả | |------------|-------------| | `onboardingId` | Định danh duy nhất của 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ấn để mở rộng) ```javascript // onboardingStarted { "name": "onboarding_started", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "welcome_screen", "screen_index": 0, "total_screens": 4 } } // screenPresented { "name": "screen_presented", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "interests_screen", "screen_index": 2, "total_screens": 4 } } // screenCompleted { "name": "screen_completed", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 1, "total_screens": 4 }, "params": { "element_id": "profile_form", "reply": "success" } } // secondScreenPresented { "name": "second_screen_presented", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 1, "total_screens": 4 } } // userEmailCollected { "name": "user_email_collected", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "profile_screen", "screen_index": 1, "total_screens": 4 } } // onboardingCompleted { "name": "onboarding_completed", "meta": { "onboarding_id": "onboarding_123", "screen_cid": "final_screen", "screen_index": 3, "total_screens": 4 } } ```
--- # File: 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." --- Khi người dùng trả lời câu hỏi trong bài kiểm tra hoặc nhập dữ liệu vào một trường nhập liệu, phương thức `onStateUpdatedAction` sẽ được gọi. Bạn có thể lưu hoặc xử lý loại trường đó trong code của mình. Ví dụ: ```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).
Ví dụ về dữ liệu đã lưu (định dạng có thể khác nhau tùy theo cách triển khai của bạn) ```javascript // Example of a saved select action { "elementId": "preference_selector", "meta": { "onboardingId": "onboarding_123", "screenClientId": "preferences_screen", "screenIndex": 1, "screensTotal": 3 }, "params": { "type": "select", "value": { "id": "option_1", "value": "premium", "label": "Premium Plan" } } } // Example of a saved multi-select action { "elementId": "interests_selector", "meta": { "onboardingId": "onboarding_123", "screenClientId": "interests_screen", "screenIndex": 2, "screensTotal": 3 }, "params": { "type": "multiSelect", "value": [ { "id": "interest_1", "value": "sports", "label": "Sports" }, { "id": "interest_2", "value": "music", "label": "Music" } ] } } // Example of a saved input action { "elementId": "name_input", "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "screensTotal": 3 }, "params": { "type": "input", "value": { "type": "text", "value": "John Doe" } } } // Example of a saved date picker action { "elementId": "birthday_picker", "meta": { "onboardingId": "onboarding_123", "screenClientId": "profile_screen", "screenIndex": 0, "screensTotal": 3 }, "params": { "type": "datePicker", "value": { "day": 15, "month": 6, "year": 1990 } } } ```
## Các trường hợp sử dụng \{#use-cases\} ### Bổ sung dữ liệu vào hồ sơ người dùng \{#enrich-user-profiles-with-data\} Nếu bạn muốn liên kết ngay dữ liệu nhập vào với hồ sơ người dùng và tránh hỏi họ hai lần 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 vào trường văn bản có ID là `name` và muốn đặt giá trị của trường đó làm tên của người dùng. Ngoài ra, bạn yêu cầu họ nhập email vào trường `email`. Trong code ứng dụng, nó có thể trông như thế này: ```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 các bài kiểm tra trong onboarding, bạn cũng 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 các nhóm người dùng khác nhau. 1. [Thêm bài kiểm tra](onboarding-quizzes) trong trình xây dựng onboarding và gán ID có ý nghĩa cho các lựa chọn. 2. Xử lý các câu trả lời bài kiểm tra dựa trên ID của chúng và [đặt 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 mỗi phân khúc bạn đã tạo. 5. [Hiển thị paywall](flutter-paywalls) cho placement trong code ứng dụng của bạn. 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 action của nút đó](flutter-handling-onboarding-events#opening-a-paywall). --- # File: flutter-sdk-call-order --- --- title: "Thứ tự gọi hàm 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 hàm này trả về kết quả, SDK chưa có trạng thái. Mọi lệnh gọi 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 liên quan đến hành động người dùng cho đến khi `identify` trả về kết quả. Các lệnh gọi chạy đua với nó sẽ thất bại với [`#3006 profileWasChanged`](error-handling-on-flutter-react-native-unity#custom-network-codes), hoặc rơi vào 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à thông tin sở hữu cài đặt không phải lúc nào cũng được chuyển sang hồ sơ đã xác định. Nếu ứng dụng 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ơ ẩn danh. Các SDK MMP và analytics (AppsFlyer, Adjust, Branch, PostHog) áp dụng 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 một hồ sơ ẩ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. Về các lưu ý riêng của AppsFlyer, xem [AppsFlyer](appsflyer). ## Thứ tự đúng \{#the-correct-order\} Hướng đi của bạn phụ thuộc vào hai điều: thời điểm bạn biết customer user ID và việc bạn có sử dụng SDK MMP hoặc analytics 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ỉ bắt buộc nếu bạn tích hợp SDK MMP hoặc analytics (AppsFlyer, Adjust, Branch, PostHog). - **Bước 4**: Chỉ bắt buộc nếu ứng dụng xác thực người dùng và thu thập customer user ID sau khi khởi chạy. Nếu bạn đã có customer user ID lúc khởi chạy ứng dụng, hãy truyền trực tiếp vào `activate()` (bước 2a). Hướng này không bao giờ tạo hồ sơ ẩn danh, vì vậy 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 SDK MMP hoặc analytics của bạn (AppsFlyer, Adjust, PostHog, Branch) | Khởi chạy ứng dụng, đầu tiên | Chờ callback UID của MMP, ví dụ `getAppsFlyerUID`. | | 2a | `Adapty().activate(configuration: ...)` với `withCustomerUserId` được thiết lập trong configuration | Khởi chạy ứng dụng, sau bước 1, nếu bạn đã có customer user ID | Được khuyến nghị. Không bao giờ tạo hồ sơ ẩn danh. | | 2b | `Adapty().activate(configuration: ...)` không có `withCustomerUserId` | Khởi chạy ứ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ơ ẩn danh. | | 3 | `Adapty().setIntegrationIdentifier(key: ..., value: ...)` cho từng MMP | Sau bước 2, trước mọi lệnh gọi liên quan đến hành động người dùng | Bắt buộc để MMP ID gắn vào đúng hồ sơ. | | 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 hướng 2b có xác thực | Luôn dùng `await`. Các lệnh gọi đồng thời trong lúc `identify` sẽ tạo ra `#3006 profileWasChanged`. | | 5 | `getPaywall`, `getPaywallProducts`, `restorePurchases`, `makePurchase`, `updateAttribution`, `updateProfile` | Sau bước 4 nếu bạn gọi `identify`; nếu không thì sau bước 3 (hoặc bước 2 nếu không có MMP) | Các lệnh gọi này cần một hồ sơ ổn định. | :::important Bỏ qua các bước này sẽ gây ra mất quyền truy cập premium cho người dùng cũ, thiếu `appsflyer_id` trên hồ sơ, 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ớ cache và các mẫu fallback 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, và fallback một cách linh hoạt khi mạng chậm. Các quy tắc dưới đây bao gồm thời điểm, bộ nhớ cache và các mẫu fallback để đạt được điều đó. :::tip Các quy tắc này giả định rằng `Adapty().activate()` và `Adapty().identify()` đã hoàn tất. Xem [Thứ tự gọi trong Flutter SDK](flutter-sdk-call-order). ::: ## Quy tắc và những lỗi thường gặp \{#rules-and-pitfalls\} | Nên làm | Không nên làm | Lý do | |---|---|---| | Tải placement bạn sắp hiển thị. | Tải trước tất cả các placement đồng thời khi khởi động. | Tải trước hàng loạt sẽ chặn luồng chính và gây màn hình đen trong thời gian đó. | | Gọi `getPaywall` sau khi attribution đã có cơ hội xử lý xong — ví dụ, 1–2 giây sau khi `activate` hoặc sau khi `didUpdateProfileStream` kích hoạt. | Gọi `getPaywall` trong `main()` trước `runApp`. | Attribution chưa được ghi nhận. Paywall sẽ được xác định theo đối tượng mặc định và âm thầm bỏ qua các phân khúc cũng như cá nhân hóa ASA. | | Đặt `loadTimeout` và cấu hình [paywall dự phòng](fallback-paywalls) cho mọi placement. | Chờ `getPaywall` vô thời hạn. | Nếu không có timeout, người dùng kết nối kém sẽ thấy màn hình trắng cho đến khi mạng phục hồi — hoặc họ sẽ thoát ứng dụng. | Xem [Tải paywall và sản phẩm](fetch-paywalls-and-products-flutter) để tham khảo các tham số `fetchPolicy` và `loadTimeout`, và [Placements](placements) để chọn đúng placement. ## Tối ưu cho kết nối kém \{#tune-for-poor-connectivity\} Đối với các thị trường có kết nối 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\} --- no_index: true --- 1. Mở [App Store Connect](https://appstoreconnect.apple.com/apps). Chọn ứng dụng của bạn và điều hướng đến phần **General** → **App Information**. 2. Sao chép **Bundle ID** trong mục **General Information**. 3. Mở tab [**App settings** -> **iOS SDK**](https://app.adapty.io/settings/ios-sdk) từ menu trên cùng của Adapty và dán giá trị vừa sao chép vào trường **Bundle ID**. 4. Quay lại trang **App information** trong App Store Connect và sao chép **Apple ID** tại đó. 5. Trên trang [**App settings** -> **iOS SDK**](https://app.adapty.io/settings/ios-sdk) trong Adapty dashboard, dán ID vào trường **Apple app ID**. ## Bước 2. Kiểm tra sản phẩm \{#step-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: 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-01T16:30:13.651Z_ _Successfully processed: 42/42 files_